From eeec52ad74bb4cc9ce2a45cd858d27dd5a0585d0 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 25 Mar 2026 15:55:51 -0700 Subject: [PATCH 1/5] Fix Codex skill injection to use ~/.codex/skills/ instead of cwd The Codex adapter was the only one injecting skills into /.agents/skills/, polluting the project's git repo. All other adapters (Gemini, Cursor, etc.) use a home-based directory. This changes the Codex adapter to inject into ~/.codex/skills/ (resolved via resolveSharedCodexHomeDir) to match the established pattern. Co-Authored-By: Paperclip --- packages/adapters/codex-local/src/index.ts | 2 +- packages/adapters/codex-local/src/server/execute.ts | 12 ++++++------ packages/adapters/codex-local/src/server/skills.ts | 2 +- server/src/__tests__/codex-local-execute.test.ts | 6 +++--- server/src/__tests__/codex-local-skill-sync.test.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts index 58511eb6..7eb4351a 100644 --- a/packages/adapters/codex-local/src/index.ts +++ b/packages/adapters/codex-local/src/index.ts @@ -42,7 +42,7 @@ Notes: - Prompts are piped via stdin (Codex receives "-" prompt argument). - If instructionsFilePath is configured, Paperclip prepends that file's contents to the stdin prompt on every run. - Codex exec automatically applies repo-scoped AGENTS.md instructions from the active workspace. Paperclip cannot suppress that discovery in exec mode, so repo AGENTS.md files may still apply even when you only configured an explicit instructionsFilePath. -- Paperclip injects desired local skills into the active workspace's ".agents/skills" directory at execution time so Codex can discover "$paperclip" and related skills without coupling them to the user's login home. +- Paperclip injects desired local skills into ~/.codex/skills/ (the Codex home directory) at execution time so Codex can discover "$paperclip" and related skills without polluting the project working directory. - Unless explicitly overridden in adapter config, Paperclip runs Codex with a per-company managed CODEX_HOME under the active Paperclip instance and seeds auth/config from the shared Codex home (the CODEX_HOME env var, when set, or ~/.codex). - Some model/tool combinations reject certain effort levels (for example minimal with web search enabled). - When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling. diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 8fa0d72c..a35caf4b 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -21,7 +21,7 @@ import { runChildProcess, } from "@paperclipai/adapter-utils/server-utils"; import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js"; -import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir } from "./codex-home.js"; +import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir, resolveSharedCodexHomeDir } from "./codex-home.js"; import { resolveCodexDesiredSkillNames } from "./skills.js"; const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); @@ -135,8 +135,8 @@ async function pruneBrokenUnavailablePaperclipSkillSymlinks( } } -function resolveCodexWorkspaceSkillsDir(cwd: string): string { - return path.join(cwd, ".agents", "skills"); +function resolveCodexSkillsHome(): string { + return path.join(resolveSharedCodexHomeDir(), "skills"); } type EnsureCodexSkillsInjectedOptions = { @@ -157,7 +157,7 @@ export async function ensureCodexSkillsInjected( const skillsEntries = allSkillsEntries.filter((entry) => desiredSet.has(entry.key)); if (skillsEntries.length === 0) return; - const skillsHome = options.skillsHome ?? resolveCodexWorkspaceSkillsDir(process.cwd()); + const skillsHome = options.skillsHome ?? resolveCodexSkillsHome(); await fs.mkdir(skillsHome, { recursive: true }); const linkSkill = options.linkSkill; for (const entry of skillsEntries) { @@ -273,11 +273,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise { "company-1", "codex-home", ); - const workspaceSkill = path.join(workspace, ".agents", "skills", "paperclip"); + const homeSkill = path.join(sharedCodexHome, "skills", "paperclip"); await fs.mkdir(workspace, { recursive: true }); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile(path.join(sharedCodexHome, "auth.json"), '{"token":"shared"}\n', "utf8"); @@ -284,7 +284,7 @@ describe("codex execute", () => { expect(await fs.realpath(isolatedAuth)).toBe(await fs.realpath(path.join(sharedCodexHome, "auth.json"))); expect((await fs.lstat(isolatedConfig)).isFile()).toBe(true); expect(await fs.readFile(isolatedConfig, "utf8")).toBe('model = "codex-mini-latest"\n'); - expect((await fs.lstat(workspaceSkill)).isSymbolicLink()).toBe(true); + expect((await fs.lstat(homeSkill)).isSymbolicLink()).toBe(true); expect(logs).toContainEqual( expect.objectContaining({ stream: "stdout", @@ -371,7 +371,7 @@ describe("codex execute", () => { const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload; expect(capture.codexHome).toBe(explicitCodexHome); - expect((await fs.lstat(path.join(workspace, ".agents", "skills", "paperclip"))).isSymbolicLink()).toBe(true); + expect((await fs.lstat(path.join(sharedCodexHome, "skills", "paperclip"))).isSymbolicLink()).toBe(true); await expect(fs.lstat(path.join(paperclipHome, "instances", "worktree-1", "codex-home"))).rejects.toThrow(); } finally { if (previousHome === undefined) delete process.env.HOME; diff --git a/server/src/__tests__/codex-local-skill-sync.test.ts b/server/src/__tests__/codex-local-skill-sync.test.ts index b809ebf8..49c239c9 100644 --- a/server/src/__tests__/codex-local-skill-sync.test.ts +++ b/server/src/__tests__/codex-local-skill-sync.test.ts @@ -43,7 +43,7 @@ describe("codex local skill sync", () => { expect(before.desiredSkills).toContain(paperclipKey); expect(before.entries.find((entry) => entry.key === paperclipKey)?.required).toBe(true); expect(before.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured"); - expect(before.entries.find((entry) => entry.key === paperclipKey)?.detail).toContain(".agents/skills"); + expect(before.entries.find((entry) => entry.key === paperclipKey)?.detail).toContain("~/.codex/skills/"); }); it("does not persist Paperclip skills into CODEX_HOME during sync", async () => { From 623ab1c3ea0c3c03fb68bb6a65b9218d99810dad Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 25 Mar 2026 16:04:53 -0700 Subject: [PATCH 2/5] Fix skill injection to use effective CODEX_HOME, not shared home The previous commit incorrectly used resolveSharedCodexHomeDir() (~/.codex) but Codex runs with CODEX_HOME set to a per-company managed home under ~/.paperclip/instances/. Skills injected into ~/.codex/skills/ would not be discoverable by Codex. Now uses effectiveCodexHome directly. Co-Authored-By: Paperclip --- packages/adapters/codex-local/src/server/execute.ts | 8 ++++---- packages/adapters/codex-local/src/server/skills.ts | 2 +- server/src/__tests__/codex-local-execute.test.ts | 4 ++-- server/src/__tests__/codex-local-skill-sync.test.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index a35caf4b..7d23dbbf 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -21,7 +21,7 @@ import { runChildProcess, } from "@paperclipai/adapter-utils/server-utils"; import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js"; -import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir, resolveSharedCodexHomeDir } from "./codex-home.js"; +import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir } from "./codex-home.js"; import { resolveCodexDesiredSkillNames } from "./skills.js"; const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); @@ -135,8 +135,8 @@ async function pruneBrokenUnavailablePaperclipSkillSymlinks( } } -function resolveCodexSkillsHome(): string { - return path.join(resolveSharedCodexHomeDir(), "skills"); +function resolveCodexSkillsDir(codexHome: string): string { + return path.join(codexHome, "skills"); } type EnsureCodexSkillsInjectedOptions = { @@ -273,7 +273,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { "company-1", "codex-home", ); - const homeSkill = path.join(sharedCodexHome, "skills", "paperclip"); + const homeSkill = path.join(isolatedCodexHome, "skills", "paperclip"); await fs.mkdir(workspace, { recursive: true }); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile(path.join(sharedCodexHome, "auth.json"), '{"token":"shared"}\n', "utf8"); @@ -371,7 +371,7 @@ describe("codex execute", () => { const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload; expect(capture.codexHome).toBe(explicitCodexHome); - expect((await fs.lstat(path.join(sharedCodexHome, "skills", "paperclip"))).isSymbolicLink()).toBe(true); + expect((await fs.lstat(path.join(explicitCodexHome, "skills", "paperclip"))).isSymbolicLink()).toBe(true); await expect(fs.lstat(path.join(paperclipHome, "instances", "worktree-1", "codex-home"))).rejects.toThrow(); } finally { if (previousHome === undefined) delete process.env.HOME; diff --git a/server/src/__tests__/codex-local-skill-sync.test.ts b/server/src/__tests__/codex-local-skill-sync.test.ts index 49c239c9..55568bef 100644 --- a/server/src/__tests__/codex-local-skill-sync.test.ts +++ b/server/src/__tests__/codex-local-skill-sync.test.ts @@ -43,7 +43,7 @@ describe("codex local skill sync", () => { expect(before.desiredSkills).toContain(paperclipKey); expect(before.entries.find((entry) => entry.key === paperclipKey)?.required).toBe(true); expect(before.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured"); - expect(before.entries.find((entry) => entry.key === paperclipKey)?.detail).toContain("~/.codex/skills/"); + expect(before.entries.find((entry) => entry.key === paperclipKey)?.detail).toContain("$CODEX_HOME/skills/"); }); it("does not persist Paperclip skills into CODEX_HOME during sync", async () => { From f6ac6e47c4129e6b5f9e77b117ae1c4efe55c9f9 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 25 Mar 2026 16:05:57 -0700 Subject: [PATCH 3/5] Clarify docs: skills go to $CODEX_HOME/skills/, defaulting to ~/.codex Addresses Greptile P2 review comment. Co-Authored-By: Paperclip --- packages/adapters/codex-local/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts index 7eb4351a..0115be06 100644 --- a/packages/adapters/codex-local/src/index.ts +++ b/packages/adapters/codex-local/src/index.ts @@ -42,7 +42,7 @@ Notes: - Prompts are piped via stdin (Codex receives "-" prompt argument). - If instructionsFilePath is configured, Paperclip prepends that file's contents to the stdin prompt on every run. - Codex exec automatically applies repo-scoped AGENTS.md instructions from the active workspace. Paperclip cannot suppress that discovery in exec mode, so repo AGENTS.md files may still apply even when you only configured an explicit instructionsFilePath. -- Paperclip injects desired local skills into ~/.codex/skills/ (the Codex home directory) at execution time so Codex can discover "$paperclip" and related skills without polluting the project working directory. +- Paperclip injects desired local skills into $CODEX_HOME/skills/ (defaulting to ~/.codex/skills/) at execution time so Codex can discover "$paperclip" and related skills without polluting the project working directory. - Unless explicitly overridden in adapter config, Paperclip runs Codex with a per-company managed CODEX_HOME under the active Paperclip instance and seeds auth/config from the shared Codex home (the CODEX_HOME env var, when set, or ~/.codex). - Some model/tool combinations reject certain effort levels (for example minimal with web search enabled). - When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling. From 4c6b9c190bb5be8ccd346886bdca439335e87159 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 25 Mar 2026 16:09:09 -0700 Subject: [PATCH 4/5] Fix stale reference to resolveCodexSkillsHome in fallback path The default fallback in ensureCodexSkillsInjected still referenced the old function name. Updated to use resolveCodexSkillsDir with shared home as fallback. Co-Authored-By: Paperclip --- packages/adapters/codex-local/src/server/execute.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 7d23dbbf..be4606c8 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -21,7 +21,7 @@ import { runChildProcess, } from "@paperclipai/adapter-utils/server-utils"; import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js"; -import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir } from "./codex-home.js"; +import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir, resolveSharedCodexHomeDir } from "./codex-home.js"; import { resolveCodexDesiredSkillNames } from "./skills.js"; const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); @@ -157,7 +157,7 @@ export async function ensureCodexSkillsInjected( const skillsEntries = allSkillsEntries.filter((entry) => desiredSet.has(entry.key)); if (skillsEntries.length === 0) return; - const skillsHome = options.skillsHome ?? resolveCodexSkillsHome(); + const skillsHome = options.skillsHome ?? resolveCodexSkillsDir(resolveSharedCodexHomeDir()); await fs.mkdir(skillsHome, { recursive: true }); const linkSkill = options.linkSkill; for (const entry of skillsEntries) { From 80766e589ca12a133aab8a2c0f044662aa0af69a Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 25 Mar 2026 20:46:05 -0700 Subject: [PATCH 5/5] Clarify docs: skills go to the effective CODEX_HOME, not ~/.codex The previous documentation parenthetical "(defaulting to ~/.codex/skills/)" was misleading because Paperclip almost always sets CODEX_HOME to a per-company managed home. Update index.ts docs, skills.ts detail string, and execute.ts inline comment to make the runtime path unambiguous. Co-Authored-By: Paperclip --- packages/adapters/codex-local/src/index.ts | 2 +- packages/adapters/codex-local/src/server/execute.ts | 2 ++ packages/adapters/codex-local/src/server/skills.ts | 2 +- server/src/__tests__/codex-local-skill-sync.test.ts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts index 0115be06..10cf6fe9 100644 --- a/packages/adapters/codex-local/src/index.ts +++ b/packages/adapters/codex-local/src/index.ts @@ -42,7 +42,7 @@ Notes: - Prompts are piped via stdin (Codex receives "-" prompt argument). - If instructionsFilePath is configured, Paperclip prepends that file's contents to the stdin prompt on every run. - Codex exec automatically applies repo-scoped AGENTS.md instructions from the active workspace. Paperclip cannot suppress that discovery in exec mode, so repo AGENTS.md files may still apply even when you only configured an explicit instructionsFilePath. -- Paperclip injects desired local skills into $CODEX_HOME/skills/ (defaulting to ~/.codex/skills/) at execution time so Codex can discover "$paperclip" and related skills without polluting the project working directory. +- Paperclip injects desired local skills into the effective CODEX_HOME/skills/ directory at execution time so Codex can discover "$paperclip" and related skills without polluting the project working directory. In managed-home mode (the default) this is ~/.paperclip/instances//companies//codex-home/skills/; when CODEX_HOME is explicitly overridden in adapter config, that override is used instead. - Unless explicitly overridden in adapter config, Paperclip runs Codex with a per-company managed CODEX_HOME under the active Paperclip instance and seeds auth/config from the shared Codex home (the CODEX_HOME env var, when set, or ~/.codex). - Some model/tool combinations reject certain effort levels (for example minimal with web search enabled). - When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling. diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index be4606c8..35c681ee 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -273,6 +273,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise { expect(before.desiredSkills).toContain(paperclipKey); expect(before.entries.find((entry) => entry.key === paperclipKey)?.required).toBe(true); expect(before.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured"); - expect(before.entries.find((entry) => entry.key === paperclipKey)?.detail).toContain("$CODEX_HOME/skills/"); + expect(before.entries.find((entry) => entry.key === paperclipKey)?.detail).toContain("CODEX_HOME/skills/"); }); it("does not persist Paperclip skills into CODEX_HOME during sync", async () => {