Repository navigation
feat(claude): SANDY_CLAUDE_ACTIVITY — a per-session turn-state record - #487
Merged
Merged
Conversation
Each Claude Code session's state (working, waiting, idle, ended) and its running subagent count are recorded to ~/.claude/activity/ in the sandbox: activity-latest.json keyed by session id, and one JSONL line per change in a daily file kept 14 days. Zero tokens and no prompt change: the hook prints nothing. The hooks are registered in Claude Code's policy settings (/etc/claude-code/managed-settings.json), so they run beside the user's own hooks and the agent cannot unregister them. That file is now written fresh under the launch's own directory (#400) and carries the hooks whether or not skip-permissions is on. An image predating the helper warns that nothing is recorded (label sandy.claude_activity) and still launches. Measured on Claude Code 2.1.310 with a pty rig: an interrupted turn (Esc, or a denied permission prompt) fires no hook at all, so it is surfaced through last_event_ts rather than guessed; SubagentStop also fires for internal agents that never started, so subagents are counted by agent id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rappdw
added a commit
that referenced
this pull request
Oct 25, 2026
* docs: handoff for the codex session activity record (follow-up to #487) A design note for whoever implements SANDY_CODEX_ACTIVITY on this branch: what #487 built and where, what codex's hooks offer per its docs (unmeasured), a proposed design, the measurements to take first, and the tests to mirror. To be replaced by the real spec section when the work lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(codex): record session activity through managed hooks --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Unit: OBS-EMITTERS-STEP1
What
A new passive key,
SANDY_CLAUDE_ACTIVITY(default1, Claude only). It records each Claude Code session's turn state to~/.claude/activity/in the sandbox, next torate-limits/. A reader can then tell which sessions are working without asking them. It costs no tokens and changes no prompt: the hook prints nothing.activity-latest.jsonis{"v":1,"sessions":{<session_id>: entry}}. Writes are atomic and taken under a lock, so sessions that share one~/.claudenever lose an update.activity-YYYY-MM-DD.jsonlgets one line per change of state or of subagent count, never one per event. Files are kept for 14 days.{"v":1,"ts","sandbox","agent":"claude","session_id","session_name","seq","event","state","subagents_active","detail"}.stateis one ofworking,waiting,idleorended.seqis per session, so a reader can spot a gap.last_event_ts.Full spec: SPECIFICATION.md C.2, "Session activity record".
How
/etc/claude-code/managed-settings.json). Claude Code runs the hooks from every settings source, so the user's own hooks stay untouched, and the agent cannot unregister these.$SANDBOX_DIR/launch/<id>/claude-policy/, still mounted:ro. Its contents now depend on the launch config, so it is no longer rewritten in place (~/.claude.json single-file bind mount forces Claude Code's non-atomic write path; torn file parks daemon sessions on a config-error modal #400).skipAutoPermissionPromptis unchanged.sandy-claude-activity 2>/dev/null; exit 0. It exits 0 even when the helper is missing, so a hook can never block a tool.sandy.claude_activity) gets a warning and still starts.Measured, not assumed (Claude Code 2.1.310, pty rig running the real TUI)
Stopand noNotification, even 240 s later. The state therefore staysworking/waitinguntil the next event, andlast_event_tsis what makes that visible.Notificationcomes in two types that matter here:permission_prompt, which maps towaiting;idle_prompt, which fires about 60 s afterStop.SubagentStopalso fires for Claude Code's internal agents, whose ids never had aSubagentStart. Subagents are therefore counted by agent id, not with a counter.Stop.-pfiresSessionStart,UserPromptSubmit,StopandSessionEnd.Tests
test/test_claude_activity.py(15 tests, run by §210(1)) runs the shipped helper, extracted from the Dockerfile.base heredoc. It covers:seq, and subagent ids;O_NOFOLLOWs, prune, retention, the:romount, the per-launch path, the dropped Notification event, theexit 0, the skip gate, the stale warning, the label, and the claude gate. Every one was killed.mainand on this branch: group results are identical, and the branch adds §210's 11 passes. The container group fails the same way on both, because it needs real Docker.lint-bash32.shpasses./etc/claude-codeis:rothere, and user namespaces are blocked). The rig proved the same hooks work through user settings. The proof for the policy layer is a new check in integration section 7: after the headless turn,claude/activity/must show a session that went working, then idle.Host runs needed:
🤖 Generated with Claude Code