Windows cached install can leave broken npm workspace bin shims for omo ulw-loop
Summary
On Windows, the LazyCodex/OMO cached plugin install can leave npm-generated workspace bin shims under the plugin cache's node_modules/.bin. Those shims are not the installer-owned command surface and can point through workspace package paths that are missing or invalid after cache promotion.
The user-visible symptom in this Codex session was that omo ulw-loop status --json was not reliably invokable from Git Bash:
omo ulw-loop status --json failed with omo: command not found.
- The cache-local npm shim at
~/.codex/plugins/cache/sisyphuslabs/omo/4.13.0/node_modules/.bin/omo-ulw-loop pointed at a missing package path: node_modules/@code-yeongyu/codex-ulw-loop/dist/cli.js.
- The actual cached component CLI worked when invoked directly from
components/ulw-loop/dist/cli.js, so the component itself was present and functional.
Current source exposes the ULW component as the omo bin, but the failure class is the same: npm can create node_modules/.bin/omo / omo.cmd inside the cache, while the installer already creates the authoritative Codex bin links through linkCachedPluginBins().
Environment
- OS: Windows, Git Bash in Codex Desktop
- LazyCodex/OMO cache observed locally:
sisyphuslabs/omo/4.13.0
- LazyCodex repo checked:
code-yeongyu/lazycodex at 4a15494
- Source owner checked:
code-yeongyu/oh-my-openagent via LazyCodex src submodule at 65715d1
- Upstream OpenAI Codex comparison: clean
openai/codex checkout had no lazycodex, omo, or ulw-loop implementation surface
Reproduction Evidence
In the affected Windows Codex project session:
omo ulw-loop status --json
# /usr/bin/bash: line 1: omo: command not found
The cache-local npm shim was also invalid:
~/.codex/plugins/cache/sisyphuslabs/omo/4.13.0/node_modules/.bin/omo-ulw-loop
→ node_modules/@code-yeongyu/codex-ulw-loop/dist/cli.js
→ missing target
Direct component invocation succeeded:
"/c/Program Files/nodejs/node" \
/c/Users/chang/.codex/plugins/cache/sisyphuslabs/omo/4.13.0/components/ulw-loop/dist/cli.js \
ulw-loop status --session-id airport-visa-20260624 --json
Observed JSON summary:
{
"ok": true,
"summary": {
"total": 12,
"pending": 12,
"in_progress": 0
}
}
Root Cause
packages/omo-codex/scripts/install/cache.mjs copies a plugin source tree into a versioned cache, rewrites local file dependencies, then runs:
await maybeRunNpmInstall(tempPath, runCommand, ["install", "--omit=dev"]);
For workspace plugins, npm can create root node_modules/.bin/* shims for package-owned bins. Those shims are internal npm install artifacts, not the installer-managed command links. They can point through workspace package paths such as node_modules/@code-yeongyu/codex-ulw-loop/dist/cli.js, which are not reliable after marketplace cache copy/promotion.
The installer already has the correct command-link mechanism:
linkCachedPluginBins({ binDir, pluginRoot: plugin.path, platform })
That function discovers package bins inside the cached plugin and links them to the real component target, such as components/ulw-loop/dist/cli.js.
Proposed Fix
After the cached npm install --omit=dev, remove npm-created .bin shims whose command names are owned by packages inside the cached plugin. Leave unrelated dependency shims intact.
This makes the installer-managed bin links the single reliable command surface and prevents agents/users from discovering a cache-local shim that points at an invalid workspace path.
Verified Patch
Patch applies to the source owner checkout code-yeongyu/oh-my-openagent at 65715d1:
diff --git a/packages/omo-codex/scripts/install-cache-copy.test.mjs b/packages/omo-codex/scripts/install-cache-copy.test.mjs
index 8d407ea..3efe875 100644
--- a/packages/omo-codex/scripts/install-cache-copy.test.mjs
+++ b/packages/omo-codex/scripts/install-cache-copy.test.mjs
@@ -4,7 +4,7 @@ import { basename, join } from "node:path";
import test from "node:test";
import { installCachedPlugin } from "./install/cache.mjs";
-import { makeTempDir } from "./install-test-fixtures.mjs";
+import { makeTempDir, writeJson } from "./install-test-fixtures.mjs";
test("#given source plugin has a stale npm lockfile #when caching plugin #then lockfile is regenerated rather than copied", async () => {
// given
@@ -29,6 +29,50 @@ test("#given source plugin has a stale npm lockfile #when caching plugin #then l
await assert.rejects(stat(join(installed.path, "package-lock.json")));
});
+test("#given npm creates workspace bin shims in the cache #when caching plugin #then plugin-owned shims are removed", async () => {
+ // given
+ const root = await makeTempDir();
+ const codexHome = join(root, "codex-home");
+ const sourceRoot = join(root, "plugin");
+ const componentRoot = join(sourceRoot, "components", "ulw-loop");
+ await mkdir(join(componentRoot, "dist"), { recursive: true });
+ await writeJson(join(sourceRoot, "package.json"), {
+ name: "@scope/omo",
+ version: "0.1.0",
+ workspaces: ["components/ulw-loop"],
+ });
+ await writeJson(join(componentRoot, "package.json"), {
+ name: "@code-yeongyu/codex-ulw-loop",
+ version: "0.1.0",
+ bin: {
+ omo: "./dist/cli.js",
+ },
+ });
+ await writeFile(join(componentRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
+
+ // when
+ const installed = await installCachedPlugin({
+ codexHome,
+ marketplaceName: "debug",
+ name: "omo",
+ sourcePath: sourceRoot,
+ version: "0.1.0",
+ runCommand: async (_command, args, options) => {
+ if (args.join(" ") !== "install --omit=dev") return;
+ const npmBinDir = join(options.cwd, "node_modules", ".bin");
+ await mkdir(npmBinDir, { recursive: true });
+ await writeFile(join(npmBinDir, "omo"), "#!/bin/sh\nnode ../@code-yeongyu/codex-ulw-loop/dist/cli.js \"$@\"\n");
+ await writeFile(join(npmBinDir, "omo.cmd"), '@echo off\r\nnode "%~dp0\\..\\@code-yeongyu\\codex-ulw-loop\\dist\\cli.js" %*\r\n');
+ await writeFile(join(npmBinDir, "other-tool.cmd"), "@echo off\r\necho preserved\r\n");
+ },
+ });
+
+ // then
+ await assert.rejects(stat(join(installed.path, "node_modules", ".bin", "omo")));
+ await assert.rejects(stat(join(installed.path, "node_modules", ".bin", "omo.cmd")));
+ assert.equal(await readFile(join(installed.path, "node_modules", ".bin", "other-tool.cmd"), "utf8"), "@echo off\r\necho preserved\r\n");
+});
+
test("#given existing cache #when npm install fails #then previous active cache is preserved", async () => {
// given
const root = await makeTempDir();
diff --git a/packages/omo-codex/scripts/install/cache.mjs b/packages/omo-codex/scripts/install/cache.mjs
index 5f10b51..3a43e88 100644
--- a/packages/omo-codex/scripts/install/cache.mjs
+++ b/packages/omo-codex/scripts/install/cache.mjs
@@ -19,6 +19,7 @@ export async function installCachedPlugin({ buildSource = true, codexHome, marke
await copyDirectory(sourcePath, tempPath, shouldCopyPluginPath);
await rewriteCachedPackageLocalFileDependencies(tempPath, sourcePath);
await maybeRunNpmInstall(tempPath, runCommand, ["install", "--omit=dev"]);
+ await removeCachedManagedNpmBinShims(tempPath);
await rewriteCachedMcpManifest(tempPath, sourcePath);
await rewriteCachedManifestRoot(tempPath, tempPath, targetPath);
await promoteDirectory(tempPath, targetPath, renameDirectory);
@@ -87,6 +88,19 @@ async function maybeRunNpmBuild(cwd, runCommand) {
await runCommand("npm", ["run", "build"], { cwd });
}
+async function removeCachedManagedNpmBinShims(pluginRoot) {
+ const binLinks = await discoverPackageBins(pluginRoot);
+ if (binLinks.length === 0) return;
+ const npmBinDir = join(pluginRoot, "node_modules", ".bin");
+ if (!(await exists(npmBinDir))) return;
+ const managedBinNames = new Set(binLinks.map((link) => link.name));
+ for (const name of managedBinNames) {
+ for (const suffix of ["", ".cmd", ".ps1"]) {
+ await rm(join(npmBinDir, `${name}${suffix}`), { force: true });
+ }
+ }
+}
+
function createTempSiblingPath(targetPath) {
return join(dirname(targetPath), `.tmp-${basename(targetPath)}-${process.pid}-${Date.now()}`);
}
Verification
Red check before the fix:
node --test scripts/install-cache-copy.test.mjs
✖ #given npm creates workspace bin shims in the cache #when caching plugin #then plugin-owned shims are removed
AssertionError [ERR_ASSERTION]: Missing expected rejection.
Green checks after the fix:
node --test scripts/install-cache-copy.test.mjs scripts/install-mcp-runtime.test.mjs
tests 9, pass 9, fail 0
node --test --test-name-pattern "Windows platform|custom Windows command shim" scripts/install-bin-links.test.mjs
tests 2, pass 2, fail 0
I also attempted a broader installer test run. The unrelated Linux-symlink cases failed on this Windows host with EPERM: operation not permitted, symlink ..., which is a local Windows symlink privilege limitation rather than a regression from this patch. The Windows command-shim cases passed.
Related Issues
I did not find an exact open duplicate for the cache-local npm workspace shim problem.
Adjacent but distinct:
This issue or PR was generated by LazyCodex.
Tag: lazycodex-generated
Windows cached install can leave broken npm workspace bin shims for
omo ulw-loopSummary
On Windows, the LazyCodex/OMO cached plugin install can leave npm-generated workspace bin shims under the plugin cache's
node_modules/.bin. Those shims are not the installer-owned command surface and can point through workspace package paths that are missing or invalid after cache promotion.The user-visible symptom in this Codex session was that
omo ulw-loop status --jsonwas not reliably invokable from Git Bash:omo ulw-loop status --jsonfailed withomo: command not found.~/.codex/plugins/cache/sisyphuslabs/omo/4.13.0/node_modules/.bin/omo-ulw-looppointed at a missing package path:node_modules/@code-yeongyu/codex-ulw-loop/dist/cli.js.components/ulw-loop/dist/cli.js, so the component itself was present and functional.Current source exposes the ULW component as the
omobin, but the failure class is the same: npm can createnode_modules/.bin/omo/omo.cmdinside the cache, while the installer already creates the authoritative Codex bin links throughlinkCachedPluginBins().Environment
sisyphuslabs/omo/4.13.0code-yeongyu/lazycodexat4a15494code-yeongyu/oh-my-openagentvia LazyCodexsrcsubmodule at65715d1openai/codexcheckout had nolazycodex,omo, orulw-loopimplementation surfaceReproduction Evidence
In the affected Windows Codex project session:
omo ulw-loop status --json # /usr/bin/bash: line 1: omo: command not foundThe cache-local npm shim was also invalid:
Direct component invocation succeeded:
"/c/Program Files/nodejs/node" \ /c/Users/chang/.codex/plugins/cache/sisyphuslabs/omo/4.13.0/components/ulw-loop/dist/cli.js \ ulw-loop status --session-id airport-visa-20260624 --jsonObserved JSON summary:
{ "ok": true, "summary": { "total": 12, "pending": 12, "in_progress": 0 } }Root Cause
packages/omo-codex/scripts/install/cache.mjscopies a plugin source tree into a versioned cache, rewrites local file dependencies, then runs:For workspace plugins, npm can create root
node_modules/.bin/*shims for package-owned bins. Those shims are internal npm install artifacts, not the installer-managed command links. They can point through workspace package paths such asnode_modules/@code-yeongyu/codex-ulw-loop/dist/cli.js, which are not reliable after marketplace cache copy/promotion.The installer already has the correct command-link mechanism:
That function discovers package bins inside the cached plugin and links them to the real component target, such as
components/ulw-loop/dist/cli.js.Proposed Fix
After the cached
npm install --omit=dev, remove npm-created.binshims whose command names are owned by packages inside the cached plugin. Leave unrelated dependency shims intact.This makes the installer-managed bin links the single reliable command surface and prevents agents/users from discovering a cache-local shim that points at an invalid workspace path.
Verified Patch
Patch applies to the source owner checkout
code-yeongyu/oh-my-openagentat65715d1:Verification
Red check before the fix:
Green checks after the fix:
I also attempted a broader installer test run. The unrelated Linux-symlink cases failed on this Windows host with
EPERM: operation not permitted, symlink ..., which is a local Windows symlink privilege limitation rather than a regression from this patch. The Windows command-shim cases passed.Related Issues
I did not find an exact open duplicate for the cache-local npm workspace shim problem.
Adjacent but distinct:
dist/cli.jsartifacts.This issue or PR was generated by LazyCodex.
Tag: lazycodex-generated