Repository navigation
fix(claude): hex's headless runs say they're hex, not the Agent SDK - #112
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
microscope reviewed
|
| Check | Result |
|---|---|
| Correctness | 3 issues (3 low) new, 2 open, 2 code objects reviewed |
| Architecture, Standards, and Engineering Philosophy | No violations: a small, consistent env-label change |
| Approvability | Needs human approval |
Approvability: Not low-risk. It changes how every headless Claude run hex starts is launched and labeled: bin/session, the hub's daemon environment, the memory agent, the voice check and chat compaction. That label decides how usage is billed against Anthropic's rate limits. There are also five open findings. One is a regression: bin/session now only checks $1 for --bg, so job x claude --bg … gets mislabeled and skips folder trust, and the test for that case was weakened. The others are behavior gaps: chat turns aren't labeled as the description says, the label still leaks to daemons started by child processes, -p builds started from a session's Bash tool aren't covered, and -p runs with a terminal on stdout get no label. A person should read it before it merges.
Resolved since the last review:
bin/session:13
Outside the diff's lines:
bin/session:12 Low: The last commit changes bg to read only $1. Any launch that doesn't put --bg first is now treated as a foreground run. With no terminal it gets CLAUDE_CODE_ENTRYPOINT=hex exported and also skips the folder-trust step. bin/job builds exactly that shape: [session, "-n", name, ...command.slice(1)], so job x claude --bg … runs bin/session -n x --bg … and comes out labeled hex. The test that covered this case ('-n', 'x/deck', '--bg', '-p' → unset) was changed to '--bg', '-p' instead of being kept, so nothing catches it. The hub is safe because it always puts --bg first. Fix: scan the arguments up to -- for --bg, as the previous commit did, or make bin/job put a --bg it is given first. Then add the -n … --bg case back to the test.
Architecture, Standards, and Engineering Philosophy
I checked the diff against the four standards: service boundaries, dependency direction, public API changes, and engineering philosophy / tech stack. Nothing violates them.
- Service boundaries / dependency direction: The PR adds no imports.
channels/voice/check.tsbuilds its env inline instead of importing anything new. Channels already import fromhub/all over the repo, so even a shared helper there wouldn't have broken the layering. - Public API:
inherited()inhub/claude.tskeeps its signature and return shape. ItsPRIVATEfilter now also drops one more variable,CLAUDE_CODE_ENTRYPOINT. Its only callers are inside the hub (hub/claude.tsandhub/hub.ts:312), and nothing outside depends on that variable passing through. - Simplicity: Each change is a one-line env override at the spot where hex starts Claude. This follows the inline filtering style
hub/observe.tsandbin/chatalready used.bin/sessionuses a singleif/elsethat labels only runs with no terminal that aren't--bg, and clears the label in every other case. No new layers or indirection. One small thing: the env-filter line incheck.tsnow matches the one inobserve.tsexactly. A shared helper could replace both later, but at this size the copy is acceptable and I'm not flagging it. - Tech stack: No new dependencies.
…which Anthropic's 5-hour limit charges more
…a background session's prompt never relabels it
…g sits, and a background session drops an inherited label
74fdd52 to
98f208d
Compare
…d it, and only $1 marks a background session again
Anthropic's 5-hour limit charges
claude -pruns, which Claude Code labelssdk-cli, about 1.6x to 1.8x more per token than interactive ones (anthropics/claude-code#97074; Theo's A/B in pingdotgg/t3code#24649). Weekly limits are not affected. Over the last 7 days on one hex install,bin/jobagents were about 80% of its Claude usage, all assdk-cli.This labels every headless Claude run hex starts
CLAUDE_CODE_ENTRYPOINT=hex, the way pingdotgg/t3code#28410 labels T3 Code'st3-code:bin/sessionfor any run with no terminal that isn't--bg(bin/jobagents), the memory agent (hub/observe.ts), the voice check, and chat compaction. Thread sessions and chat turns start--bg, and they and terminal sessions drop any label they inherited, so they keep Claude Code's owncli; the hub also stops passing its own on to the daemon. Aclaude -pa session runs straight from its Bash tool, outsidebin/job, still goes assdk-cli.Checked on the wire against a local capture server with Claude Code 2.1.317: unlabeled
claude -psendsUser-Agent: claude-cli/2.1.317 (external, sdk-cli)andcc_entrypoint=sdk-cli; labeled, it sendshexin both. It ignoresCLAUDE_CODE_ENTRYPOINT=cliin-pmode, so a run can't claim to be interactive.Note
Headless Claude runs started by hex are now labeled
CLAUDE_CODE_ENTRYPOINT=hexinstead ofsdk-clisdk-cliabout 1.6–1.8x more per token. On one install,bin/jobagents made up about 80% of Claude usage.bin/sessionsetshexwhen there's no terminal and the run isn't--bg. Otherwise it unsets the variable, so background and terminal sessions keep Claude Code's owncli.hub/observe.ts), the voice check (channels/voice/check.ts) and chat compaction (bin/chat) also sethex. The voice check now gets a filtered environment too, withoutHEX_*andCLAUDECODE*.hub/claude.tsaddsCLAUDE_CODE_ENTRYPOINTtoPRIVATE, so sessions started by the hub's daemon don't inherit whatever label started the hub.plugin/tests/session.test.tstests the label for-p, non-terminal,---escaped--bgand real--bgruns.sdk-cli, which is undocumented and could change.🔬 microscope summarized 0fe1911. 2 files reviewed, 7 issues evaluated, 4 filtered, 3 comments posted
Filtered issues
bin/session:71Duplicate of the already-reported bin/session:72 item. That item says the same thing: the label depends only on[ ! -t 1 ]while Claude Code pickssdk-cliin-pmode, so a-prun with a terminal on stdout getsunsetand is billed assdk-cli. It also notes the test can't catch this and gives the same fix: detect-p/--printin the arguments.bin/session:71The code reads as described:bg=${1:-}only looks at the first argument. But this isn't a real defect. Treating--bgas a background launch only when it comes first is a choice the codebase already made: the trust-prompt logic on line 13 has always used it, and the PR's tests lock it in (launched('-p','--','--bg')gives hex,launched('--bg',...)gives unset). Every launcher of background sessions puts--bgfirst:hub/claude.tsstart() and the buzz/chat tests. The PR's claim is about "background thread sessions", which are the hub's, and those do drop the label. The failing case needsjob x claude --bg. bin/job is documented and nudged towardjob <unit> claude -p "<prompt>", and a--bgjob would background and end the systemd unit at once, so this is a made-up scenario. Even then, the only effect is that the short-lived--bgclient gets labeledhexinstead ofcli. That doesn't charge more against the 5-hour limit, and since Claude's daemon runs the backgrounded session, the client's label likely never reaches it.bin/session:71This was already reported (bin/session:72). That report names the same problem:[ ! -t 1 ]decides the label, so abin/session -prun from a terminal goes to theunsetbranch and Claude Code reportssdk-cli. It gives the same fix too: also label the run when-p/--printappears before--.bin/session:12The bug is real but already reported.bin/session:12setsbg=${1:-}, andbin/job:48builds[session, "-n", name, ...], so a--bgpassed through a job is never seen. The run then skips the trust step and gets labeledhexwhen it has no terminal. The third already-reported item (bin/session:12) covers this exactly: the same$1-only parsing, the same bin/job-ncall shape, the same two effects (skipped folder trust and thehexlabel being exported), the same test swap from'-n','x/deck','--bg','-p'to'--bg','-p', and the same fix. This is a duplicate.Note
Set
CLAUDE_CODE_ENTRYPOINT=hexfor all headless Claude launchesHeadless Claude runs launched from Hex were inheriting an entrypoint label that identified them as Agent SDK runs. This PR sets
CLAUDE_CODE_ENTRYPOINT=hexin every subprocess environment that already filtersHEX_/CLAUDECODEvariables: bin/chat, check.ts, observe.ts, and the session launcher in bin/session.inherited()filter in claude.ts also dropsCLAUDE_CODE_ENTRYPOINT, so hub-spawned callers never inherit a stale value.-pand non--p) and--bginvocations.Macroscope summarized 0fe1911.