Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Resources/bin/cmux-claude-wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ claude_passthrough_option_flag() {

claude_builtin_command_name() {
case "$1" in
agents|auth|auto-mode|config|api-key|daemon|doctor|install|mcp|\
agents|attach|auth|auto-mode|config|api-key|daemon|doctor|install|mcp|\
experimental-next|plugin|plugins|project|rc|remote-control|setup-token|\
ultrareview|update|upgrade)
return 0
Expand Down
18 changes: 18 additions & 0 deletions tests/test_claude_wrapper_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,23 @@ def test_command_like_invocations_bypass_hook_injection(failures: list[str]) ->
expect("--session-id" not in real_argv, f"agents after global option passthrough: expected no --session-id injection, got {real_argv}", failures)


def test_hidden_attach_subcommand_bypasses_hook_injection(failures: list[str]) -> None:
# `claude attach <id>` is a real subcommand (the attach door for `--bg`
# background sessions) but is hidden from `claude --help`, so it's easy to
# miss when refreshing the builtin-command list. Injecting
# --session-id/--settings ahead of it makes the CLI treat "attach" as the
# [prompt] positional and mint a fresh session instead of attaching.
code, real_argv, _, stderr, _, node_options, _, _, _, _ = run_wrapper(
socket_state="live",
argv=["attach", "abc12345"],
)
expect(code == 0, f"attach passthrough: wrapper exited {code}: {stderr}", failures)
expect(real_argv == ["attach", "abc12345"], f"attach passthrough: expected raw argv, got {real_argv}", failures)
expect("--settings" not in real_argv, f"attach passthrough: expected no --settings injection, got {real_argv}", failures)
expect("--session-id" not in real_argv, f"attach passthrough: expected no --session-id injection, got {real_argv}", failures)
expect(node_options == "__UNSET__", f"attach passthrough: expected no NODE_OPTIONS injection, got {node_options!r}", failures)


def test_passthrough_flags_bypass_hook_injection(failures: list[str]) -> None:
for flag in ("--help", "--version", "-h", "-v"):
code, real_argv, _, stderr, _, node_options, _, _, _, _ = run_wrapper(
Expand Down Expand Up @@ -1893,6 +1910,7 @@ def main() -> int:
test_live_socket_empty_settings_warns_instead_of_silent_drop(failures)
test_plain_claude_launch_argv_has_no_empty_argument(failures)
test_command_like_invocations_bypass_hook_injection(failures)
test_hidden_attach_subcommand_bypasses_hook_injection(failures)
test_passthrough_flags_bypass_hook_injection(failures)
test_agents_subcommand_removes_cmux_terminal_fingerprint(failures)
test_hooks_disabled_preserves_cmux_terminal_env_for_custom_hooks(failures)
Expand Down