Summary
headroom wrap claude installs a SessionStart self-heal hook that cannot ever run. The command it writes names the wrong binary for the subcommand, and on this machine also names a path that does not exist. Two independent defects in one generated line.
Found live on 2026-07-26 while wiring the proxy for real use.
What got written
headroom wrap claude created .claude/settings.local.json in the repo working tree:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "/usr/bin/claude wrap selfheal --marker headroom-wrap-selfheal",
"timeout": 10
}
]
}
]
},
"env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8787" }
}
Defect 1: the path does not exist
/bin/sh: 1: /usr/bin/claude: not found
The real binary on this host is /config/.local/bin/claude (a symlink to Claude Code 2.1.220). Nothing is installed at /usr/bin/claude. Claude Code reported this as a non-blocking hook failure at every session start.
Defect 2: wrong binary entirely, which the path fix exposes
Correcting the path is not enough. Running the fixed command:
$ /config/.local/bin/claude wrap selfheal --marker headroom-wrap-selfheal
error: unknown option '--marker'
(Did you mean --bare?)
wrap selfheal is a headroom subcommand, not a claude one. The generated hook points a Claude Code binary at a Headroom command. Even on a host where /usr/bin/claude does exist, this hook could never have worked.
The correct form resolves cleanly:
$ /path/to/.venv/bin/headroom wrap selfheal --marker headroom-wrap-selfheal
$ echo $?
0
Why this matters more than a noisy log line
The hook is not cosmetic. Per its own docstring at headroom/cli/wrap.py:1331-1345, _selfheal_dead_wrap_base_url() exists to clear a project-local ANTHROPIC_BASE_URL left pointing at a dead proxy:
When wrap claude persists ANTHROPIC_BASE_URL=<proxy> into .claude/settings.local.json and the proxy later dies via hard reboot / SIGKILL, no signal/atexit cleanup fires, so the stale URL lingers and bricks a later bare claude with ConnectionRefused.
So the failure mode is exactly the one the hook was written to prevent. wrap claude persists a base URL into the repo, the hook that would clean it up cannot run, and after any hard proxy death every subsequent claude invocation in that directory fails with ConnectionRefused until someone finds and edits the file by hand. The safety net is installed and inert.
Where it comes from
headroom/cli/wrap.py:1357 _wrap_selfheal_hook_command() builds the string from resolve_headroom_command().
headroom/install/runtime.py:69-75 resolve_headroom_command() returns shutil.which("headroom"), falling back to [sys.executable, "-m", "headroom.cli"].
Neither of those should be able to yield /usr/bin/claude. Verified on this host:
| lookup |
result |
shutil.which("headroom") inside uv run |
<repo>/.venv/bin/headroom |
command -v headroom |
/usr/local/bin/headroom → /opt/uv-tools/headroom-ai/bin/headroom |
command -v claude |
/config/.local/bin/claude |
/usr/bin/headroom |
does not exist |
/usr/bin/claude |
does not exist |
So the written value matches no resolver output available here. Either another code path writes this hook, or the value is inherited from an earlier install and rewritten without re-resolution. Root cause is not fully established and is the first thing to pin down.
Acceptance criteria
Workaround applied locally
.claude/settings.local.json hand-edited to point at <repo>/.venv/bin/headroom. Confirmed exit 0. Not committed; the file is untracked.
Related
Separate concern, same file, worth its own issue: wrap claude writes env.ANTHROPIC_BASE_URL into a repo-scoped .claude/settings.local.json without prompting, so every later Claude Code session started in that directory silently routes through the proxy. That is a durable, directory-scoped change to a working tree made as a side effect of one command.
Found while remediating #15.
Summary
headroom wrap claudeinstalls aSessionStartself-heal hook that cannot ever run. The command it writes names the wrong binary for the subcommand, and on this machine also names a path that does not exist. Two independent defects in one generated line.Found live on 2026-07-26 while wiring the proxy for real use.
What got written
headroom wrap claudecreated.claude/settings.local.jsonin the repo working tree:{ "hooks": { "SessionStart": [ { "matcher": "startup|resume", "hooks": [ { "type": "command", "command": "/usr/bin/claude wrap selfheal --marker headroom-wrap-selfheal", "timeout": 10 } ] } ] }, "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8787" } }Defect 1: the path does not exist
The real binary on this host is
/config/.local/bin/claude(a symlink to Claude Code 2.1.220). Nothing is installed at/usr/bin/claude. Claude Code reported this as a non-blocking hook failure at every session start.Defect 2: wrong binary entirely, which the path fix exposes
Correcting the path is not enough. Running the fixed command:
wrap selfhealis a headroom subcommand, not a claude one. The generated hook points a Claude Code binary at a Headroom command. Even on a host where/usr/bin/claudedoes exist, this hook could never have worked.The correct form resolves cleanly:
Why this matters more than a noisy log line
The hook is not cosmetic. Per its own docstring at
headroom/cli/wrap.py:1331-1345,_selfheal_dead_wrap_base_url()exists to clear a project-localANTHROPIC_BASE_URLleft pointing at a dead proxy:So the failure mode is exactly the one the hook was written to prevent.
wrap claudepersists a base URL into the repo, the hook that would clean it up cannot run, and after any hard proxy death every subsequentclaudeinvocation in that directory fails with ConnectionRefused until someone finds and edits the file by hand. The safety net is installed and inert.Where it comes from
headroom/cli/wrap.py:1357_wrap_selfheal_hook_command()builds the string fromresolve_headroom_command().headroom/install/runtime.py:69-75resolve_headroom_command()returnsshutil.which("headroom"), falling back to[sys.executable, "-m", "headroom.cli"].Neither of those should be able to yield
/usr/bin/claude. Verified on this host:shutil.which("headroom")insideuv run<repo>/.venv/bin/headroomcommand -v headroom/usr/local/bin/headroom→/opt/uv-tools/headroom-ai/bin/headroomcommand -v claude/config/.local/bin/claude/usr/bin/headroom/usr/bin/claudeSo the written value matches no resolver output available here. Either another code path writes this hook, or the value is inherited from an earlier install and rewritten without re-resolution. Root cause is not fully established and is the first thing to pin down.
Acceptance criteria
/usr/bin/claude; the two resolvers above cannot account for it.wrap selfheal --marker.wrap, not left behind.Workaround applied locally
.claude/settings.local.jsonhand-edited to point at<repo>/.venv/bin/headroom. Confirmed exit 0. Not committed; the file is untracked.Related
Separate concern, same file, worth its own issue:
wrap claudewritesenv.ANTHROPIC_BASE_URLinto a repo-scoped.claude/settings.local.jsonwithout prompting, so every later Claude Code session started in that directory silently routes through the proxy. That is a durable, directory-scoped change to a working tree made as a side effect of one command.Found while remediating #15.