Skip to content

feat(claude): SANDY_CLAUDE_ACTIVITY — a per-session turn-state record - #487

Merged
rappdw merged 1 commit into
mainfrom
feat/claude-activity
Oct 25, 2026
Merged

rappdw merged 1 commit into
mainfrom
feat/claude-activity

Conversation

@rappdw

@rappdw rappdw commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Unit: OBS-EMITTERS-STEP1

What

A new passive key, SANDY_CLAUDE_ACTIVITY (default 1, Claude only). It records each Claude Code session's turn state to ~/.claude/activity/ in the sandbox, next to rate-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.json is {"v":1,"sessions":{<session_id>: entry}}. Writes are atomic and taken under a lock, so sessions that share one ~/.claude never lose an update.
  • activity-YYYY-MM-DD.jsonl gets one line per change of state or of subagent count, never one per event. Files are kept for 14 days.
  • Line format: {"v":1,"ts","sandbox","agent":"claude","session_id","session_name","seq","event","state","subagents_active","detail"}.
    • state is one of working, waiting, idle or ended.
    • seq is per session, so a reader can spot a gap.
    • Each entry also carries last_event_ts.

Full spec: SPECIFICATION.md C.2, "Session activity record".

How

  • Registration: the hooks go in the policy settings (/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.
  • Policy file now written fresh each launch: it moves to $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).
    • It carries the hooks whether or not skip-permissions is on.
    • skipAutoPermissionPrompt is unchanged.
  • Hook command: 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.
  • Stale image: an image without the helper (label sandy.claude_activity) gets a warning and still starts.

Measured, not assumed (Claude Code 2.1.310, pty rig running the real TUI)

  • Esc mid-turn, and denying a permission prompt, fire no hook at all. There is no Stop and no Notification, even 240 s later. The state therefore stays working/waiting until the next event, and last_event_ts is what makes that visible.
  • Notification comes in two types that matter here:
    • permission_prompt, which maps to waiting;
    • idle_prompt, which fires about 60 s after Stop.
  • SubagentStop also fires for Claude Code's internal agents, whose ids never had a SubagentStart. Subagents are therefore counted by agent id, not with a counter.
  • A background subagent outlives its turn's Stop.
  • Headless -p fires SessionStart, UserPromptSubmit, Stop and SessionEnd.

Tests

  • test/test_claude_activity.py (15 tests, run by §210(1)) runs the shipped helper, extracted from the Dockerfile.base heredoc. It covers:
    • states, seq, and subagent ids;
    • prune and retention;
    • bad input;
    • never writing through a planted link;
    • 40 concurrent hooks with no lost update.
  • §210(2)–(10) run the real policy-settings block and stale-image block under stubs.
  • §111(2) and (5) were reworked for the new block.
  • Mutation testing: 24 mutations were run on scratch copies, covering the lock, the subagent decrement, compact handling, the O_NOFOLLOWs, prune, retention, the :ro mount, the per-launch path, the dropped Notification event, the exit 0, the skip gate, the stale warning, the label, and the claude gate. Every one was killed.
  • Full suite run in the sandbox with a docker stub, on main and 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.sh passes.
  • Not verifiable in the sandbox: whether Claude Code actually runs hooks from the policy layer (/etc/claude-code is :ro there, 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:

bash test/run-tests.sh
SANDY_INTEG_ONLY=7 bash test/run-integration-tests.sh

🤖 Generated with Claude Code

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
rappdw merged commit d07b323 into main Oct 25, 2026
4 checks passed
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>
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