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
Builds on #2 and #3. Since relay and workforce always control the Claude Code spawn, burn can install hooks per-invocation via --settings rather than mutating the user's ~/.claude/settings.json globally. This is the production path for new sessions; delta-math from #2 remains the fallback for historical sessions.
Verified:
claude --settings <file-or-json> accepts a JSON string inline (not just a path).
claude --session-id <uuid> pre-assigns the session UUID so it's known to the spawner before Claude boots.
Why hooks
Claude Code's hooks fire with the full payload the hook needs — PostToolUse carries tool_response content verbatim. Confirmed in lazyagent's parser (internal/claude/parser.go:58-61). That removes the delta-math guesswork for per-tool-call sizing:
Reads a hook payload JSON from stdin, parses, appends to the ledger. Session ID comes from the payload (session_id field); no need to thread it as a flag. Idempotent via the dedup index from #4.
Hook script is a one-liner — it just execs burn ingest --runtime claude. See lazyagent's scripts/claude-hook.sh for the pattern.
B. Settings builder for spawners
New export from @relayburn/spawner (new package, or folded into @relayburn/cli):
import{buildClaudeHookSettings}from'@relayburn/spawner';const{ sessionId, settings }=buildClaudeHookSettings({// optional: override the burn binary path (defaults to locating 'burn' on PATH)burnBin?: string,});// settings is a JSON string like: '{"hooks":{"PostToolUse":[{...}], ...}}'// sessionId is a pre-generated UUID
Spawner usage:
const{ sessionId, settings }=buildClaudeHookSettings();// Stamp metadata against the session BEFORE claude starts.// Works because @relayburn/ledger.stamp already handles stamp-before-turn.awaitstamp({ sessionId },{ workflowId, agentId, persona, tier });// Spawn with pre-assigned session ID and injected hook settings.spawn('claude',['--session-id',sessionId,'--settings',settings,
...existingArgs,]);
No new env vars, no files written to disk, no mutation of ~/.claude/settings.json.
C. Hook events to register
MVP subset (everything the existing reader uses):
PostToolUse — the load-bearing event. Gives per-tool-call output size directly.
PreToolUse — timestamps and tool_use_id for duration math.
UserPromptSubmit — turn boundaries.
SubagentStop — subagent tree closure (pairs with #B).
SessionEnd — final flush marker.
Defer Stop, Notification, PostToolUseFailure to v0.2 unless there's a specific signal they enable.
Integration points
workforce — spawn site at packages/cli/src/cli.ts:579. Args builder at packages/harness-kit/src/harness.ts:69-153. Extend buildInteractiveSpec to append --session-id and --settings when the harness is claude.
packages/sdk/src/workflows/runner.ts:5887 (interactive agents via relay.spawnPty)
Both need the same treatment: call buildClaudeHookSettings(), pass flags through, stamp metadata against the returned session ID. Relay's existing env-var threading (AGENT_NAME, RELAY_API_KEY, AGENT_CHANNELS at runner.ts:5863-5868) is orthogonal — leave it alone.
Migration note: relay already has a wall-clock-based token estimator at src/cost/tracker.ts writing to ~/.agent-relay/usage.jsonl. Once this issue ships, that file becomes burn's ledger and CostTracker retires. Out of scope for this issue — file a follow-up in relay for the retirement.
Acceptance
buildClaudeHookSettings() returns a valid {sessionId, settings} pair; spawning claude with those flags starts a session that streams hook payloads to burn ingest.
A round-trip test: spawn, run one tool call, exit. Ledger contains at least one TurnRecord from the hook path with an exact tool_response byte count recorded.
Ledger entries from the hook path and the JSONL-reader path for the same session are reconcilable — same sessionId, same messageId, no double-count. (Prefer hook data when both present.)
Stamped metadata (workflowId, agentId) attaches correctly to all turns of the session when queried.
Context
Builds on #2 and #3. Since relay and workforce always control the Claude Code spawn, burn can install hooks per-invocation via
--settingsrather than mutating the user's~/.claude/settings.jsonglobally. This is the production path for new sessions; delta-math from #2 remains the fallback for historical sessions.Verified:
claude --settings <file-or-json>accepts a JSON string inline (not just a path).claude --session-id <uuid>pre-assigns the session UUID so it's known to the spawner before Claude boots.Why hooks
Claude Code's hooks fire with the full payload the hook needs —
PostToolUsecarriestool_responsecontent verbatim. Confirmed in lazyagent's parser (internal/claude/parser.go:58-61). That removes the delta-math guesswork for per-tool-call sizing:PreToolUse→ tool name, inputs, wall-clock start, tool_use_idPostToolUse→ exact response bytes, duration, success, tool_use_id correlationPostToolUseFailure→ distinct failure pathUserPromptSubmit→ clean turn boundarySubagentStop→ clean subagent boundarySessionStart/SessionEnd→ lifecycleDesign
A. New ingest command
Reads a hook payload JSON from stdin, parses, appends to the ledger. Session ID comes from the payload (
session_idfield); no need to thread it as a flag. Idempotent via the dedup index from #4.Hook script is a one-liner — it just execs
burn ingest --runtime claude. See lazyagent'sscripts/claude-hook.shfor the pattern.B. Settings builder for spawners
New export from
@relayburn/spawner(new package, or folded into@relayburn/cli):Spawner usage:
No new env vars, no files written to disk, no mutation of
~/.claude/settings.json.C. Hook events to register
MVP subset (everything the existing reader uses):
PostToolUse— the load-bearing event. Gives per-tool-call output size directly.PreToolUse— timestamps and tool_use_id for duration math.UserPromptSubmit— turn boundaries.SubagentStop— subagent tree closure (pairs with #B).SessionEnd— final flush marker.Defer
Stop,Notification,PostToolUseFailureto v0.2 unless there's a specific signal they enable.Integration points
workforce — spawn site at
packages/cli/src/cli.ts:579. Args builder atpackages/harness-kit/src/harness.ts:69-153. ExtendbuildInteractiveSpecto append--session-idand--settingswhen the harness isclaude.relay — two spawn sites:
packages/sdk/src/workflows/process-spawner.ts:68(non-interactive agent steps)packages/sdk/src/workflows/runner.ts:5887(interactive agents viarelay.spawnPty)Both need the same treatment: call
buildClaudeHookSettings(), pass flags through, stamp metadata against the returned session ID. Relay's existing env-var threading (AGENT_NAME,RELAY_API_KEY,AGENT_CHANNELSatrunner.ts:5863-5868) is orthogonal — leave it alone.Migration note: relay already has a wall-clock-based token estimator at
src/cost/tracker.tswriting to~/.agent-relay/usage.jsonl. Once this issue ships, that file becomes burn's ledger andCostTrackerretires. Out of scope for this issue — file a follow-up in relay for the retirement.Acceptance
buildClaudeHookSettings()returns a valid{sessionId, settings}pair; spawning claude with those flags starts a session that streams hook payloads toburn ingest.TurnRecordfrom the hook path with an exacttool_responsebyte count recorded.--resume <session-id>) appends new turns without duplicating existing ones (relies on Reader infrastructure: incremental cursors and git-canonical project keys #4's dedup).sessionId, samemessageId, no double-count. (Prefer hook data when both present.)Depends on
Unblocks
burn waste) ships with tool-level precision on new sessions rather than delta estimates.