Skip to content

fix(claude): hex's headless runs say they're hex, not the Agent SDK - #112

Merged
AVGVSTVS96 merged 4 commits into
mainfrom
entrypoint-hex
Nov 3, 2026
Merged

AVGVSTVS96 merged 4 commits into
mainfrom
entrypoint-hex

Conversation

@AVGVSTVS96

@AVGVSTVS96 AVGVSTVS96 commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Anthropic's 5-hour limit charges claude -p runs, which Claude Code labels sdk-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/job agents were about 80% of its Claude usage, all as sdk-cli.

This labels every headless Claude run hex starts CLAUDE_CODE_ENTRYPOINT=hex, the way pingdotgg/t3code#28410 labels T3 Code's t3-code: bin/session for any run with no terminal that isn't --bg (bin/job agents), 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 own cli; the hub also stops passing its own on to the daemon. A claude -p a session runs straight from its Bash tool, outside bin/job, still goes as sdk-cli.

Checked on the wire against a local capture server with Claude Code 2.1.317: unlabeled claude -p sends User-Agent: claude-cli/2.1.317 (external, sdk-cli) and cc_entrypoint=sdk-cli; labeled, it sends hex in both. It ignores CLAUDE_CODE_ENTRYPOINT=cli in -p mode, so a run can't claim to be interactive.

Note

Headless Claude runs started by hex are now labeled CLAUDE_CODE_ENTRYPOINT=hex instead of sdk-cli

  • Why: Anthropic's 5-hour limit charges runs labeled sdk-cli about 1.6–1.8x more per token. On one install, bin/job agents made up about 80% of Claude usage.
  • bin/session sets hex when there's no terminal and the run isn't --bg. Otherwise it unsets the variable, so background and terminal sessions keep Claude Code's own cli.
  • The memory agent (hub/observe.ts), the voice check (channels/voice/check.ts) and chat compaction (bin/chat) also set hex. The voice check now gets a filtered environment too, without HEX_* and CLAUDECODE*.
  • hub/claude.ts adds CLAUDE_CODE_ENTRYPOINT to PRIVATE, so sessions started by the hub's daemon don't inherit whatever label started the hub.
  • plugin/tests/session.test.ts tests the label for -p, non-terminal, ---escaped --bg and real --bg runs.
  • Risk: Low for the code. The billing gain depends on Anthropic continuing to treat a custom entrypoint label differently from 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:71 Duplicate of the already-reported bin/session:72 item. That item says the same thing: the label depends only on [ ! -t 1 ] while Claude Code picks sdk-cli in -p mode, so a -p run with a terminal on stdout gets unset and is billed as sdk-cli. It also notes the test can't catch this and gives the same fix: detect -p/--print in the arguments.
  • bin/session:71 The code reads as described: bg=${1:-} only looks at the first argument. But this isn't a real defect. Treating --bg as 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 --bg first: hub/claude.ts start() 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 needs job x claude --bg. bin/job is documented and nudged toward job <unit> claude -p "<prompt>", and a --bg job 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 --bg client gets labeled hex instead of cli. 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:71 This was already reported (bin/session:72). That report names the same problem: [ ! -t 1 ] decides the label, so a bin/session -p run from a terminal goes to the unset branch and Claude Code reports sdk-cli. It gives the same fix too: also label the run when -p/--print appears before --.
  • bin/session:12 The bug is real but already reported. bin/session:12 sets bg=${1:-}, and bin/job:48 builds [session, "-n", name, ...], so a --bg passed through a job is never seen. The run then skips the trust step and gets labeled hex when it has no terminal. The third already-reported item (bin/session:12) covers this exactly: the same $1-only parsing, the same bin/job -n call shape, the same two effects (skipped folder trust and the hex label 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=hex for all headless Claude launches

Headless Claude runs launched from Hex were inheriting an entrypoint label that identified them as Agent SDK runs. This PR sets CLAUDE_CODE_ENTRYPOINT=hex in every subprocess environment that already filters HEX_/CLAUDECODE variables: bin/chat, check.ts, observe.ts, and the session launcher in bin/session.

  • The hub's inherited() filter in claude.ts also drops CLAUDE_CODE_ENTRYPOINT, so hub-spawned callers never inherit a stale value.
  • The session script exports the variable only for non-background sessions without a terminal, and unsets it otherwise; a new test in session.test.ts covers foreground (-p and non--p) and --bg invocations.
  • Behavioral Change: background sessions and sessions with a terminal now pass no entrypoint label, where before they could inherit one from the environment.

Macroscope summarized 0fe1911.

@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hex Ready Ready Preview Nov 3, 2026 7:36am UTC

Request Review

Comment thread bin/chat Resolved
Comment thread bin/session Resolved
Comment thread bin/session Resolved
Comment thread channels/voice/check.ts Resolved
Comment thread plugin/tests/session.test.ts Resolved
@AVGVSTVS96

AVGVSTVS96 commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner Author

microscope reviewed 0fe1911

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.ts builds its env inline instead of importing anything new. Channels already import from hub/ all over the repo, so even a shared helper there wouldn't have broken the layering.
  • Public API: inherited() in hub/claude.ts keeps its signature and return shape. Its PRIVATE filter now also drops one more variable, CLAUDE_CODE_ENTRYPOINT. Its only callers are inside the hub (hub/claude.ts and hub/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.ts and bin/chat already used. bin/session uses a single if/else that 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 in check.ts now matches the one in observe.ts exactly. 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.

Comment thread bin/session Resolved
Comment thread bin/session Resolved
Comment thread bin/session Resolved
Comment thread bin/session Resolved
Comment thread plugin/tests/session.test.ts Resolved
…a background session's prompt never relabels it
…g sits, and a background session drops an inherited label
Comment thread bin/session Resolved
Comment thread bin/session Resolved
…d it, and only $1 marks a background session again
Comment thread bin/session Resolved
Comment thread hub/claude.ts Resolved
@AVGVSTVS96
AVGVSTVS96 marked this pull request as ready for review November 3, 2026 07:48
@AVGVSTVS96
AVGVSTVS96 merged commit 09811c1 into main Nov 3, 2026
7 checks passed
@AVGVSTVS96
AVGVSTVS96 deleted the entrypoint-hex branch November 3, 2026 07:48

This branch was successfully deployed

1 active deployment
Preview — 0fe19116 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant