diff --git a/mcp-watcher/README.md b/mcp-watcher/README.md index 22e6bf5..fceecde 100644 --- a/mcp-watcher/README.md +++ b/mcp-watcher/README.md @@ -49,7 +49,7 @@ won't compete with the default `~/.ping-agent` for inbox reads): ```bash mkdir -p ~/.claude-sandbox echo sandbox > ~/.claude-sandbox/ping-agent -CLAUDE_HOME=~/.claude-sandbox \ +CLAUDE_CONFIG_DIR=~/.claude-sandbox \ claude --dangerously-load-development-channels server:agent-watcher ``` diff --git a/mcp-watcher/install.sh b/mcp-watcher/install.sh index e0f7ebe..b918151 100755 --- a/mcp-watcher/install.sh +++ b/mcp-watcher/install.sh @@ -106,7 +106,7 @@ Then start Claude Code with the development flag (research preview): To use a sandbox identity instead of the default ~/.ping-agent: - CLAUDE_HOME=~/.claude-sandbox claude --dangerously-load-development-channels server:agent-watcher + CLAUDE_CONFIG_DIR=~/.claude-sandbox claude --dangerously-load-development-channels server:agent-watcher (Drop a 'ping-agent' file in ~/.claude-sandbox containing the sandbox name first.) diff --git a/mcp-watcher/src/identity.ts b/mcp-watcher/src/identity.ts index d5c65e4..a8fcdd9 100644 --- a/mcp-watcher/src/identity.ts +++ b/mcp-watcher/src/identity.ts @@ -19,8 +19,11 @@ export interface ResolvedIdentity { * Resolve this process's agent identity. * Layers, first match wins: * 1. PING_AGENT_IDENTITY env var - * 2. $CLAUDE_HOME/ping-agent (if CLAUDE_HOME is set and file exists) - * 3. ~/.ping-agent + * 2. $CLAUDE_CONFIG_DIR/ping-agent (the env var Claude Code actually reads; + * verified by `strings` on the binary, 2026-05-06 sandbox spike). + * 3. $CLAUDE_HOME/ping-agent (kept for compat with anyone setting the + * previously-documented variable). + * 4. ~/.ping-agent * * Throws if no layer resolves. */ @@ -30,9 +33,10 @@ export function resolveIdentity(env: NodeJS.ProcessEnv = process.env): ResolvedI return { agent: envId, source: "PING_AGENT_IDENTITY env var" }; } - const claudeHome = (env.CLAUDE_HOME ?? "").trim(); - if (claudeHome) { - const f = join(claudeHome, "ping-agent"); + for (const varName of ["CLAUDE_CONFIG_DIR", "CLAUDE_HOME"]) { + const dir = (env[varName] ?? "").trim(); + if (!dir) continue; + const f = join(dir, "ping-agent"); if (existsSync(f)) { const name = readFileSync(f, "utf8").trim(); if (name) { @@ -52,6 +56,6 @@ export function resolveIdentity(env: NodeJS.ProcessEnv = process.env): ResolvedI throw new Error( `agent-watcher-mcp: no identity resolved. Set PING_AGENT_IDENTITY, ` + - `$CLAUDE_HOME/ping-agent, or ${defaultFile}.`, + `$CLAUDE_CONFIG_DIR/ping-agent (or $CLAUDE_HOME/ping-agent), or ${defaultFile}.`, ); } diff --git a/mcp-watcher/test/identity.test.ts b/mcp-watcher/test/identity.test.ts index 32fe44e..fee9400 100644 --- a/mcp-watcher/test/identity.test.ts +++ b/mcp-watcher/test/identity.test.ts @@ -23,20 +23,49 @@ describe("resolveIdentity", () => { expect(r.source).toMatch(/env/i); }); - it("uses $CLAUDE_HOME/ping-agent next", () => { + it("uses $CLAUDE_CONFIG_DIR/ping-agent next", () => { writeFileSync(join(dir, ".ping-agent"), "default-agent\n"); - const claudeHome = join(dir, "claude-sandbox"); + const sandbox = join(dir, "claude-sandbox"); + mkdirSync(sandbox); + writeFileSync(join(sandbox, "ping-agent"), "sandbox\n"); + const r = resolveIdentity({ + HOME: dir, + CLAUDE_CONFIG_DIR: sandbox, + PING_AGENT_IDENTITY: "", + }); + expect(r.agent).toBe("sandbox"); + expect(r.source).toContain(sandbox); + }); + + it("falls back to $CLAUDE_HOME/ping-agent when CLAUDE_CONFIG_DIR is unset", () => { + writeFileSync(join(dir, ".ping-agent"), "default-agent\n"); + const claudeHome = join(dir, "claude-old"); mkdirSync(claudeHome); - writeFileSync(join(claudeHome, "ping-agent"), "sandbox\n"); + writeFileSync(join(claudeHome, "ping-agent"), "compat\n"); const r = resolveIdentity({ HOME: dir, CLAUDE_HOME: claudeHome, PING_AGENT_IDENTITY: "", }); - expect(r.agent).toBe("sandbox"); + expect(r.agent).toBe("compat"); expect(r.source).toContain(claudeHome); }); + it("CLAUDE_CONFIG_DIR takes precedence over CLAUDE_HOME", () => { + const sandbox = join(dir, "claude-sandbox"); + const old = join(dir, "claude-old"); + mkdirSync(sandbox); + mkdirSync(old); + writeFileSync(join(sandbox, "ping-agent"), "new\n"); + writeFileSync(join(old, "ping-agent"), "old\n"); + const r = resolveIdentity({ + HOME: dir, + CLAUDE_CONFIG_DIR: sandbox, + CLAUDE_HOME: old, + }); + expect(r.agent).toBe("new"); + }); + it("falls through to ~/.ping-agent", () => { writeFileSync(join(dir, ".ping-agent"), "bob\n"); const r = resolveIdentity({ HOME: dir }); @@ -48,12 +77,12 @@ describe("resolveIdentity", () => { expect(() => resolveIdentity({ HOME: dir })).toThrow(/no identity/); }); - it("ignores empty CLAUDE_HOME/ping-agent and falls through", () => { + it("ignores empty CLAUDE_CONFIG_DIR/ping-agent and falls through", () => { writeFileSync(join(dir, ".ping-agent"), "bob\n"); - const claudeHome = join(dir, "claude-sandbox"); - mkdirSync(claudeHome); - writeFileSync(join(claudeHome, "ping-agent"), ""); - const r = resolveIdentity({ HOME: dir, CLAUDE_HOME: claudeHome }); + const sandbox = join(dir, "claude-sandbox"); + mkdirSync(sandbox); + writeFileSync(join(sandbox, "ping-agent"), ""); + const r = resolveIdentity({ HOME: dir, CLAUDE_CONFIG_DIR: sandbox }); expect(r.agent).toBe("bob"); });