From c575ddae2d3f7e05628971bb819c14a03a09ab10 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Fri, 28 Aug 2026 21:19:49 -0700 Subject: [PATCH 1/2] Pin Claude marker hooks to launch worktree --- plugins/worktrunk/CLAUDE.md | 2 ++ plugins/worktrunk/hooks/hooks.json | 12 ++++++------ tests/integration_tests/config_show.rs | 8 ++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/plugins/worktrunk/CLAUDE.md b/plugins/worktrunk/CLAUDE.md index 5d9caa7d6..763dfb6d5 100644 --- a/plugins/worktrunk/CLAUDE.md +++ b/plugins/worktrunk/CLAUDE.md @@ -150,6 +150,8 @@ The events (Codex's `HookEventsToml` vocabulary, verified against `codex-rs/conf `Stop` fires at turn-end, so 🤖 returns to 💬 when a turn completes. `SessionEnd` clears the marker when the main thread ends. +Codex and Gemini marker commands still resolve from the hook process's cwd. When either harness exposes a stable session project directory, pass that directory to Worktrunk with global `-C`, as Claude does with `$CLAUDE_PROJECT_DIR`. + ### Accepted tradeoff: shared `skills/` exposes `wt-switch-create` Codex's mirrored `skills/` and Gemini's `${extensionPath}/skills/` both carry the entire skill set, including `wt-switch-create`, which depends on Claude session-cwd switching (`EnterWorktree`) that neither provides. Accepted: a tool loading a skill it can't act on is harmless, and a single authored `skills/` keeps the `worktrunk` skill single-source across all three tools and the docs sync. Don't add per-tool skills subtrees to exclude it. diff --git a/plugins/worktrunk/hooks/hooks.json b/plugins/worktrunk/hooks/hooks.json index 5531ca5cc..273cee90f 100644 --- a/plugins/worktrunk/hooks/hooks.json +++ b/plugins/worktrunk/hooks/hooks.json @@ -5,7 +5,7 @@ "hooks": [ { "type": "command", - "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" config state marker set 🤖 || true" + "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" -C \"$CLAUDE_PROJECT_DIR\" config state marker set 🤖 || true" } ] } @@ -16,7 +16,7 @@ "hooks": [ { "type": "command", - "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" config state marker set 💬 || true" + "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" -C \"$CLAUDE_PROJECT_DIR\" config state marker set 💬 || true" } ] } @@ -27,7 +27,7 @@ "hooks": [ { "type": "command", - "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" config state marker set 💬 || true" + "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" -C \"$CLAUDE_PROJECT_DIR\" config state marker set 💬 || true" } ] } @@ -38,7 +38,7 @@ "hooks": [ { "type": "command", - "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" config state marker set 💬 || true" + "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" -C \"$CLAUDE_PROJECT_DIR\" config state marker set 💬 || true" } ] } @@ -48,7 +48,7 @@ "hooks": [ { "type": "command", - "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" config state marker set 💬 || true" + "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" -C \"$CLAUDE_PROJECT_DIR\" config state marker set 💬 || true" } ] } @@ -59,7 +59,7 @@ "hooks": [ { "type": "command", - "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" config state marker clear || true" + "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/wt.sh\" -C \"$CLAUDE_PROJECT_DIR\" config state marker clear || true" } ] } diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index be6d232ab..1016ea21e 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3962,6 +3962,14 @@ fn test_plugin_layout_is_consolidated() { ); } + let marker_commands = all_commands + .iter() + .filter(|command| command.contains("config state marker")) + .collect::>(); + assert_eq!(marker_commands.len(), 6); + for command in marker_commands { + assert!(command.contains(r#"-C "$CLAUDE_PROJECT_DIR""#)); + } let worktree_remove_cmd = hooks_json["hooks"]["WorktreeRemove"][0]["hooks"][0]["command"] .as_str() .expect("WorktreeRemove hook must define a command"); From f6c65a8031d6ff941fa8c1b3ef19107cdcada3c2 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Fri, 28 Aug 2026 21:33:01 -0700 Subject: [PATCH 2/2] Document Claude marker launch pin --- plugins/worktrunk/CLAUDE.md | 4 ++++ tests/integration_tests/config_show.rs | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/worktrunk/CLAUDE.md b/plugins/worktrunk/CLAUDE.md index 763dfb6d5..9a8778e1f 100644 --- a/plugins/worktrunk/CLAUDE.md +++ b/plugins/worktrunk/CLAUDE.md @@ -123,6 +123,10 @@ end-to-end against codex-cli 0.144.1 (scratch marketplaces through ## Known Limitations +### Status stays on the launch worktree (Claude) + +Claude marker hooks resolve through `-C "$CLAUDE_PROJECT_DIR"`. `EnterWorktree` does not change that directory, so the marker stays on the launch worktree for the session. Sessions launched outside a repository have no activity marker. + ### Status persists after user interrupt (Claude) The Claude hooks track activity via git config (`worktrunk.state.{branch}.marker`): diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index 1016ea21e..0b4c347a9 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3966,9 +3966,20 @@ fn test_plugin_layout_is_consolidated() { .iter() .filter(|command| command.contains("config state marker")) .collect::>(); - assert_eq!(marker_commands.len(), 6); + assert_eq!( + marker_commands.len(), + 6, + "expected all 6 Claude marker hooks (UserPromptSubmit, Notification, \ + PreToolUse, PermissionRequest, Stop, SessionEnd); a newly added one \ + must be pinned too. hooks.json:\n{hooks}" + ); for command in marker_commands { - assert!(command.contains(r#"-C "$CLAUDE_PROJECT_DIR""#)); + assert!( + command.contains(r#"-C "$CLAUDE_PROJECT_DIR""#), + "marker hook must pin the directory it resolves against, or a shell \ + `cd` during a turn retargets it to another repository (#3921). \ + command:\n{command}" + ); } let worktree_remove_cmd = hooks_json["hooks"]["WorktreeRemove"][0]["hooks"][0]["command"] .as_str()