Build the hook command string written into Claude Code's settings.json. * POSIX embeds a safely quoted absolute custom config path or a portable $HOME * path. Windows explicitly invokes cmd.exe so the hook also works when Claude * falls back from Git Bash to PowerShell. The Windows command defers expansion * of custom paths to cmd.exe and disables delayed expansion, so user path bytes * never
| 2465 | * of custom paths to cmd.exe and disables delayed expansion, so user path bytes |
| 2466 | * never become source text in either outer shell. */ |
| 2467 | static int cbm_build_claude_hook_command(const char *script_name, const char *config_dir, |
| 2468 | bool windows, char *out, size_t out_sz) { |
| 2469 | if (!cbm_hook_script_name_safe(script_name) || !out || out_sz == 0U) { |
| 2470 | return CLI_ERR; |
| 2471 | } |
| 2472 | out[0] = '\0'; |
| 2473 | if (windows) { |
| 2474 | const char *base = |
| 2475 | config_dir && config_dir[0] ? "%CLAUDE_CONFIG_DIR%" : "%USERPROFILE%\\.claude"; |
| 2476 | int written = snprintf(out, out_sz, "cmd.exe /d /v:off /s /c '\"\"%s\\hooks\\%s\"\"'", base, |
| 2477 | script_name); |
| 2478 | return written > 0 && (size_t)written < out_sz ? CLI_OK : CLI_ERR; |
| 2479 | } |
| 2480 | if (config_dir && config_dir[0]) { |
| 2481 | char path[CLI_BUF_1K]; |
| 2482 | int written = snprintf(path, sizeof(path), "%s/hooks/%s", config_dir, script_name); |
| 2483 | return written > 0 && (size_t)written < sizeof(path) |
| 2484 | ? cbm_shell_quote_word(path, out, out_sz) |
| 2485 | : CLI_ERR; |
| 2486 | } |
| 2487 | int written = snprintf(out, out_sz, "\"$HOME/.claude/hooks/%s\"", script_name); |
| 2488 | return written > 0 && (size_t)written < out_sz ? CLI_OK : CLI_ERR; |
| 2489 | } |
| 2490 | |
| 2491 | static int cbm_resolve_hook_command(const char *script_name, char *out, size_t out_sz) { |
| 2492 | char env_buf[CLI_BUF_1K]; |
no test coverage detected