You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Description
`headroom wrap claude` writes `env.ANTHROPIC_BASE_URL` (or the
foundry/vertex variant) into a project's `.claude/settings.local.json`
so daemon-spawned Claude Code workers route through the local Headroom
proxy. Removal only happened in the wrap process's `finally:` block. An
unclean exit — `SIGKILL`, OOM, reboot, or terminal/tmux close (`SIGHUP`,
which was not caught; only `SIGINT`/`SIGTERM` were) — skipped that
cleanup, so the entry persisted indefinitely. Every subsequent bare
`claude` in that project then routed to the dead port and hung
indefinitely retrying it.
Closesheadroomlabs-ai#1768
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `_write_claude_wrap_base_url` now optionally stamps a sidecar marker
(`.claude/.headroom_wrap_marker.json`) recording the writer's
pid/identity, the port, and the true prior value — kept out of
`settings.local.json` itself so Headroom bookkeeping never shows up as a
stray key in a file Claude Code's own config loader parses.
- A shared `_identity_mismatch` helper (factored out of the existing
`_marker_pid_reused` proxy-client-refcounting logic) lets a marker be
judged stale: missing/invalid pid, dead pid, or a live pid whose
identity doesn't match the recorded one (PID reuse after a crash).
- `claude()` now checks for — and self-heals — a stale marker
immediately before writing a fresh entry, restoring the recorded prior
value instead of trusting a leftover from a dead session.
- `claude()` now also registers a `SIGHUP` handler (guarded via
`hasattr`, since Windows has none) alongside the existing `SIGTERM`
handler, so terminal-close triggers the same cleanup/restore path.
- `headroom unwrap claude` now reads the marker's recorded prior value
before restoring, instead of unconditionally deleting the key — so a
user's own pre-existing `ANTHROPIC_BASE_URL` (set before ever running
`wrap`) isn't blindly wiped.
- `headroom doctor` gained a new check (`check_wrap_marker_staleness`)
that flags a stale project-local marker and points at `headroom unwrap
claude` to clean it up — separate from the existing global-settings
`check_claude_routing` check.
- (Unrelated, pre-existing on `main`) reformatted
`headroom/proxy/handlers/openai.py`,
`tests/test_openai_codex_ws_lifecycle.py`, `tests/test_output_shaper.py`
— whitespace/indentation only, no logic change — since they were already
failing `ruff format --check .` on `main` before this branch touched
anything, and the repo-wide lint gate blocks on it.
Out of scope: `wrap --worktree` — no such flag or multi-worktree
`.claude` handling exists anywhere in `wrap.py` today; not adding new
surface for an aspirational scenario the issue mentions but that isn't
implemented.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_cli/test_wrap_claude_base_url.py tests/test_cli/test_unwrap_claude.py tests/test_cli/test_wrap_stale_marker.py -q
42 passed
$ pytest tests/test_cli -q
512 passed, 1 failed (test_wrap_codex_prepare_only_registers_serena_when_uvx_exists —
confirmed to fail identically on a clean checkout of main with no changes applied;
test-order flake, unrelated to this PR)
$ ruff check .
All checks passed!
$ ruff format --check .
1047 files already formatted
$ mypy headroom/cli/wrap.py headroom/cli/doctor.py
Success: no issues found in 2 source files
```
## Real Behavior Proof
- Environment: local checkout, Python 3.13, Windows.
- Exact command / steps: wrote a base_url entry + marker via
`_write_claude_wrap_base_url(..., port=8787)`, then overwrote the
marker's recorded pid with a value guaranteed not to be a live process
(simulating the crash from the issue's own repro: `headroom wrap claude
-- -p ok & ; kill -9 <wrap-pid>`). Ran
`headroom.cli.doctor.check_wrap_marker_staleness()` against that path,
then called `_check_and_clear_stale_wrap_marker()` (the same check
`claude()` now runs before writing a fresh entry).
- Observed result: `doctor`'s check correctly reports `WARN` naming the
dead pid/port and pointing at `headroom unwrap claude`. The stale-check
call then self-heals: in the "nothing existed before wrap" case the
leaked entry is removed; in a second run seeded with a real pre-existing
`ANTHROPIC_BASE_URL` (set before `wrap` ever ran), that original value
is recovered instead of being deleted. In both cases the marker file is
cleared afterward.
- Not tested: actual OS-level signal delivery (`kill -HUP` against a
real running `headroom wrap claude` subprocess) — the SIGHUP
registration is exercised via a source-inspection test instead of a live
signal, since spawning/killing the real CLI subprocess isn't practical
in this environment; verified E2E via CI's `wrap-native` jobs
(Ubuntu/macOS) which passed.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A — CLI/backend fix, no UI surface.
## Additional Notes
- Documentation checklist item left unchecked: no user-facing docs
currently describe wrap's settings.local.json write/cleanup behavior in
enough detail to need updating; happy to add a troubleshooting note if
maintainers want one.
- `wrap --worktree` handling is out of scope (see Changes Made) —
flagging in case maintainers want it tracked as a separate follow-up
issue.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
0 commit comments