From e032bcd30a91e935d3536965b48a63145ffbe2a4 Mon Sep 17 00:00:00 2001 From: Viktor Fredslund-Hansen Date: Wed, 3 Jun 2026 03:26:52 +0200 Subject: [PATCH] fix(agent-claude-code): use $CLAUDE_PROJECT_DIR for hook commands The activity/metadata lifecycle hooks were registered with relative command paths (.claude/activity-updater.sh, .claude/metadata-updater.sh) for every hook event, including PreToolUse/PostToolUse/SubagentStart/SubagentStop. When a hook fires from a sub-agent (Agent/Task) tool call, the process cwd is not the worktree root, so the shell cannot resolve the relative path: /bin/sh: 1: .claude/activity-updater.sh: not found This is non-blocking but spams the transcript and drops the .ao/activity.jsonl signal during sub-agent runs, which can cause false stuck/probe_failure detection on healthy sessions. Prefix the commands with $CLAUDE_PROJECT_DIR (exported by Claude Code for hook execution and always pointing at the worktree root). This keeps settings.json content identical across worktrees while resolving correctly regardless of the hook's cwd. Applied to both the unix (.sh) and windows (.cjs) branches; tests updated to the new contract. Fixes #2090 Co-Authored-By: Claude Opus 4.8 --- .../agent-claude-code/src/index.test.ts | 18 +++++++++++------- .../plugins/agent-claude-code/src/index.ts | 8 ++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/plugins/agent-claude-code/src/index.test.ts b/packages/plugins/agent-claude-code/src/index.test.ts index eb9fa28125..2e6e3bfdef 100644 --- a/packages/plugins/agent-claude-code/src/index.test.ts +++ b/packages/plugins/agent-claude-code/src/index.test.ts @@ -902,14 +902,18 @@ describe("hook setup — relative path (symlink-safe)", () => { return parsed.hooks.PostToolUse[0].hooks[0].command; } - it("setupWorkspaceHooks writes a relative hook command (not absolute)", async () => { + it("setupWorkspaceHooks writes a $CLAUDE_PROJECT_DIR-based hook command (not a hardcoded absolute path)", async () => { await agent.setupWorkspaceHooks!( "/Users/equinox/.worktrees/integrator/integrator-5", {} as WorkspaceHooksConfig, ); const hookCommand = getWrittenHookCommand(); - expect(hookCommand).toBe(".claude/metadata-updater.sh"); + // $CLAUDE_PROJECT_DIR resolves to the worktree root at hook-execution time, + // so the command works even when a hook fires from a sub-agent (Agent/Task) + // whose cwd is not the worktree root — while keeping settings.json content + // identical across worktrees (no hardcoded path). + expect(hookCommand).toBe("$CLAUDE_PROJECT_DIR/.claude/metadata-updater.sh"); expect(hookCommand).not.toMatch(/^\//); }); @@ -950,7 +954,7 @@ describe("hook setup — relative path (symlink-safe)", () => { expect(content1).toBe(content2); }); - it("updates an existing absolute hook path to relative", async () => { + it("updates an existing absolute hook path to the $CLAUDE_PROJECT_DIR form", async () => { mockExistsSync.mockReturnValue(true); mockReadFile.mockResolvedValue( JSON.stringify({ @@ -978,7 +982,7 @@ describe("hook setup — relative path (symlink-safe)", () => { ); const hookCommand = getWrittenHookCommand(); - expect(hookCommand).toBe(".claude/metadata-updater.sh"); + expect(hookCommand).toBe("$CLAUDE_PROJECT_DIR/.claude/metadata-updater.sh"); }); it("still writes the script file to the correct absolute filesystem path", async () => { @@ -1021,8 +1025,8 @@ describe("setupWorkspaceHooks — activity-updater (#1941)", () => { } /** Activity-updater command paths (unix vs win32) */ - const ACTIVITY_CMD_UNIX = ".claude/activity-updater.sh"; - const ACTIVITY_CMD_WIN = "node .claude/activity-updater.cjs"; + const ACTIVITY_CMD_UNIX = "$CLAUDE_PROJECT_DIR/.claude/activity-updater.sh"; + const ACTIVITY_CMD_WIN = "node $CLAUDE_PROJECT_DIR/.claude/activity-updater.cjs"; /** * Every Claude Code hook event the script knows how to translate into an @@ -1326,7 +1330,7 @@ describe("setupWorkspaceHooks on win32", () => { await agent.setupWorkspaceHooks!("C:\\\\Users\\\\dev\\\\workspace", {} as WorkspaceHooksConfig); const hookCommand = getWrittenHookCommand(); - expect(hookCommand).toBe("node .claude/metadata-updater.cjs"); + expect(hookCommand).toBe("node $CLAUDE_PROJECT_DIR/.claude/metadata-updater.cjs"); expect(hookCommand).not.toContain(".sh"); }); diff --git a/packages/plugins/agent-claude-code/src/index.ts b/packages/plugins/agent-claude-code/src/index.ts index cbbf92ed15..d9651208d1 100644 --- a/packages/plugins/agent-claude-code/src/index.ts +++ b/packages/plugins/agent-claude-code/src/index.ts @@ -1009,8 +1009,8 @@ async function setupHookInWorkspace(workspacePath: string): Promise { await writeFile(activityPath, ACTIVITY_UPDATER_SCRIPT_NODE, "utf-8"); // .cjs forces CJS regardless of workspace package.json "type"; node // invocation is required on Windows because shebangs aren't honoured. - metadataCommand = "node .claude/metadata-updater.cjs"; - activityCommand = "node .claude/activity-updater.cjs"; + metadataCommand = "node $CLAUDE_PROJECT_DIR/.claude/metadata-updater.cjs"; + activityCommand = "node $CLAUDE_PROJECT_DIR/.claude/activity-updater.cjs"; } else { const metadataPath = join(claudeDir, "metadata-updater.sh"); const activityPath = join(claudeDir, "activity-updater.sh"); @@ -1018,8 +1018,8 @@ async function setupHookInWorkspace(workspacePath: string): Promise { await writeFile(activityPath, ACTIVITY_UPDATER_SCRIPT, "utf-8"); await chmod(metadataPath, 0o755); await chmod(activityPath, 0o755); - metadataCommand = ".claude/metadata-updater.sh"; - activityCommand = ".claude/activity-updater.sh"; + metadataCommand = "$CLAUDE_PROJECT_DIR/.claude/metadata-updater.sh"; + activityCommand = "$CLAUDE_PROJECT_DIR/.claude/activity-updater.sh"; } let existingSettings: Record = {};