From eeec52ad74bb4cc9ce2a45cd858d27dd5a0585d0 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 25 Mar 2026 15:55:51 -0700 Subject: [PATCH] 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 () => {