Skip to content

feat(claude): hook-driven observed harness state with wrapper terminal writes - #324

Merged
schickling-assistant merged 14 commits into
schickling/hs-05-pifrom
schickling/hs-06-claude
Aug 24, 2026
Merged

feat(claude): hook-driven observed harness state with wrapper terminal writes#324
schickling-assistant merged 14 commits into
schickling/hs-05-pifrom
schickling/hs-06-claude

Conversation

@schickling

@schickling schickling commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

The Claude producer (#268 §C entry + §D), where no observed-state machinery existed at all.

  • hooks/claude-observe.sh (immutable hook set) + st2 driver claude-observe: hook events map to transitions — UserPromptSubmit/PreToolUse/PostToolUse → active, Stop → idle, PermissionRequest → active + blockedOn: human, SessionStart → idle. Hooks are fail-open (always exit 0; a record write failure never blocks a turn). Any payload carrying a non-empty agent_id never moves top-level state (the phantom-SubagentStop hazard, with the emergent-property caveat in a comment).

  • Blocked exit edge: the next PreToolUse/PostToolUse/Stop clears blockedOnmeasured-correct, not merely conservative: the DQ-H1 batched-permission capture (Claude Code 2.1.237, 4 turns, verbatim replay fixture) shows tool execution serializes around an open permission prompt, so the predicted batched false-clear cannot occur. The residual limit is the eventless deny path ("No" emits zero events, even with PermissionDenied registered), pinned in DQ-H1.

  • SessionObserver in the shared wrapper: heartbeat on the presence tick via a fresh writer each time (never clobbers hook-written state), with one pinned session-start timestamp so a restarted wrapper never re-stamps a predecessor session's record; terminal record with the real exit on every reaped child; and the §D ordering fix — the terminal record lands before the SIGKILL escalation of the wrapper's own group, now proven against the real wrapper binary (tests/harness_state_teardown.rs).

  • Driver-declared seats get the producer too: expand_claude renders the canonical hook registration (hooks::claude_settings_registration, consistency-tested against examples/native/agent-claude.kdl so the two surfaces cannot drift). Live seats converge without restarts — the merged settings land on the next materialization pass and take effect at the next session start.

  • The hook script resolves --catalog CATALOG-first: its sibling hooks' ST_ROOT-first order is for bus writes, and under a custom bus root declaration resolution would silently drop every transition. ptySession carries the wrapper's runtime ID end to end (ST2_CLAUDE_RUNTIME_ID → hook → --runtime-id), aliasing the identity only on driver-expanded seats.

  • Cycle-2: json-upsert gains opt-in arrays="union" so the generated hook registration joins user-declared hooks instead of clobbering them; PermissionRequest classifies its ask kind from tool_name (AskUserQuestion → question, else permission); SessionStart marks the session boundary.

  • Cycle-3: union merges supersede st2's own prior hook-set entries (recognized by the hook root they reference) instead of accumulating them across content-addressed upgrades, while foreign entries are never touched; hook writes carry the wrapper's exported incarnation token so a late hook cannot overwrite the session's terminal record; the maintained example documents its hooks-only limitation.

  • Cycle-4: hooks adopt the wrapper's exported claim (token + seq) so superseded-session stragglers are refused; hook-set supersession is structural (managed basename under any sets/sha256-* path; $ST_HOOKS only at a token boundary) — relocation-proof and never matching foreign variables.

  • Cycle-6: wrapperless seats gained working session succession (SessionStart performs the written claim, guarded so an interactive interloper can never supersede a live wrapper's record); legacy deliver "mcp" seats render the canonical hook registration their claude-session wrapper assumes; managed entries live only in their canonical group — nested in a user's group they are superseded, never doubled.

Part of the #268 stack (#319#326).

🤖 Generated with Claude Code

@schickling schickling changed the title schickling/hs 06 claude feat(claude): hook-driven observed harness state with wrapper terminal writes Aug 23, 2026
@schickling
schickling marked this pull request as ready for review August 23, 2026 18:18

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f93b3e8f0f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hooks/claude-observe.sh Outdated
Comment thread src/provider_session.rs Outdated
@schickling
schickling force-pushed the schickling/hs-06-claude branch from f93b3e8 to 25a007e Compare August 23, 2026 19:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25a007e3a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c36233d489

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/materialize.rs
Comment thread src/claude_session.rs Outdated
Comment thread examples/native/agent-claude.kdl Outdated
@schickling
schickling force-pushed the schickling/hs-06-claude branch from c36233d to c86a7f7 Compare August 23, 2026 22:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c86a7f72d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/claude_session.rs
Comment thread src/materialize.rs Outdated
Comment thread src/materialize.rs Outdated
@schickling
schickling force-pushed the schickling/hs-06-claude branch from c86a7f7 to 35fd435 Compare August 23, 2026 23:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35fd4358be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/materialize.rs Outdated
@schickling
schickling force-pushed the schickling/hs-06-claude branch from 35fd435 to 461ecc2 Compare August 23, 2026 23:35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 461ecc2a63

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/claude_session.rs Outdated
Comment thread src/driver.rs
Comment thread src/materialize.rs Outdated
@schickling
schickling force-pushed the schickling/hs-06-claude branch from 461ecc2 to 2b2d0df Compare August 24, 2026 00:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b2d0dfd4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/claude_session.rs Outdated
Comment thread src/claude_session.rs
@schickling
schickling force-pushed the schickling/hs-06-claude branch from 2b2d0df to d487e1c Compare August 24, 2026 00:37
@schickling
schickling force-pushed the schickling/hs-06-claude branch from d487e1c to 4a46358 Compare August 24, 2026 04:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a46358ddd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks.rs Outdated
fn observe(event: &str) -> serde_json::Value {
serde_json::json!([{ "hooks": [{
"type": "command",
"command": format!("$ST_HOOKS/claude-observe.sh {event}"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Quote generated Claude hook executable paths

When ST_HOOKS resolves to a supported custom or XDG state path containing whitespace, materialization expands this unquoted reference into a command such as /state root/sets/.../claude-observe.sh Stop; Claude's command runner then treats only /state as the executable, and the fail-open hook silently drops every affected transition. Shell-quote or otherwise escape the hook-set path in all generated command strings.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a7ea487 (+ snapshot 94af370) on schickling/hs-06-claude: every generated hook command double-quotes the $ST_HOOKS executable, and the managed-reference recognizer reads the quoted executable token rather than the first whitespace-delimited segment, so whitespace-containing custom/XDG roots resolve to one executable again.

Posted on behalf of @schickling
field value
agent_identity unknown
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.0.3
agent_runtime OMP 18.0.3
tooling_profile dotfiles@f33cd9c-dirty

Comment thread src/materialize.rs
Comment on lines +528 to +532
target.retain_mut(|element| {
if patch.contains(element) || !contains_owned_string(element) {
return true;
}
keep_after_supersession(element)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope managed-hook pruning to canonical Claude merges

When any hand-authored json-upsert arrays="union" targets an array containing a string that matches the managed-hook path heuristic, this branch deletes that existing element even if the patch is unrelated to Claude hooks. Because union mode is parsed as a generic json-upsert option and documented as append/deduplicate behavior, this can unexpectedly remove user data from arbitrary JSON; apply st2 hook supersession only to the canonical Claude settings registration rather than every union merge.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a7ea487 on schickling/hs-06-claude: st2-hook supersession is now applied only when the union merge targets the canonical Claude settings registration (.claude/settings.local.json); any other arrays="union" merge unions by exact equality alone and can never delete user elements that merely match the managed-path heuristic.

Posted on behalf of @schickling
field value
agent_identity unknown
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.0.3
agent_runtime OMP 18.0.3
tooling_profile dotfiles@f33cd9c-dirty

schickling and others added 3 commits August 24, 2026 16:31
…tbeat, terminal writes

Hook-driven turn transitions land through a new fail-open claude-observe.sh
(shipped in the immutable hook set) and 'st2 driver claude-observe':
UserPromptSubmit/PreToolUse/PostToolUse -> active, Stop -> idle,
PermissionRequest -> active blocked-on-human, SessionStart -> idle. Subagent
events (non-empty agent_id) never move top-level state. The shared provider
wrapper gains a SessionObserver that re-stamps the record on the presence
cadence without clobbering hook-written state, writes the terminal record on
every reaped child, and writes it BEFORE SIGKILL-escalating its own process
group so st2's own teardown no longer leaves a dead agent reading active.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…catalog-first hook resolution

Review-pass fixes: SessionObserver pins one session-start timestamp so
its per-tick fresh writers never re-stamp a predecessor session's record;
ptySession carries the wrapper's runtime ID end to end (wrapper env
ST2_CLAUDE_RUNTIME_ID -> hook script -> claude-observe --runtime-id),
aliasing the identity only on driver-expanded seats; and the hook script
resolves --catalog CATALOG-first — its sibling hooks' ST_ROOT-first order
is for bus writes, and under a custom bus root declaration resolution
would silently drop every transition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude Code 2.1.237 serializes tool execution around an open permission
prompt — no hook event fires while a prompt is up, in either batch
ordering — so the shipped blocked-exit rule is correct, not merely
conservative. The residual limit is the eventless deny path. The
measured grant sequence is replayed verbatim as a mapping fixture, and
the phantom SubagentStop's emergent shape is pinned from this build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
schickling and others added 11 commits August 24, 2026 16:31
…nsion

Driver-declared Claude seats now receive the same
.claude/settings.local.json hook registration a hand-authored seat
carries, closing the gap where they had no observed-state producer and
no lifecycle hooks at all. The registration has one canonical shape
(hooks::claude_settings_registration) shared by expansion and pinned
against the maintained example declaration by a consistency test, so the
two surfaces cannot drift. The materializer admits the new destination
verbatim — its $ST_HOOKS references are render variables, not
executable paths — and driver seats now meet the same verified-hook-set
gate hand-authored $ST_HOOKS renders always met. Render output is not
part of the launch fingerprint, so live seats converge without
disruption on their next materialization and session start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and ask classification

json-upsert gains arrays="union" (exact-equality array union, default
stays replace) so the generated hook registration joins user-declared
hooks instead of clobbering them each materialization; SessionStart marks
the writer's session boundary; PermissionRequest classifies its ask kind
from tool_name (AskUserQuestion -> question, else permission); the
catalog resolves CATALOG-first in claude-observe.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and hook-set supersession

The wrapper mints and exports its incarnation token (ST2_CLAUDE_SESSION;
wrapperless hooks fall back to Claude's own session_id), late hooks can
no longer overwrite the session's terminal record, pi gains a
terminal-only observer so the pre-escalation ended write is real for its
stop path, union merges supersede st2's own prior hook-set entries while
never touching foreign ones, and the maintained example states its
hooks-only limitation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k recognition

The observer claims the ownership sequence at construction and exports
it beside its token (ST2_CLAUDE_SESSION_SEQ); hooks adopt the pair, so a
hook straggling from a superseded session is refused. Union supersession
recognizes st2's entries structurally — a managed basename under any
set-shaped path (sets/sha256-*), or $ST_HOOKS at a token boundary — so
a relocated hook root cannot duplicate registrations and
$ST_HOOKS_SUFFIX-style foreign variables are never misclassified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…supersession

SessionObserver performs the written ownership claim at construction
(fatal if unwritable — an unclaimed writer would be refused by every
record); union supersession descends into matcher groups, removing only
the superseded managed entries so a group holding user and st2 hooks
keeps the user's, and dropping only genuine husks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion, and single-home managed leaves

A wrapperless seat's SessionStart performs the written claim (its only
session boundary — token-only writers never claim, so session 2 would
otherwise be refused forever), legacy deliver-mcp seats render the same
canonical hook registration their claude-session wrapper assumes, and a
managed entry survives only inside its canonical group — nested in a
user's group it is superseded rather than registered twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e wrapper is not

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eligibility and the written takeover are one act under the record lock;
ineligible and unwritable both degrade to token-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The spawn and liveness-check error arms end the record honestly (ended,
exit unknown, launch-error) when an observer is present, so the claim
placeholder never stands as the visible state of a launch that never ran
— while the ordinary nonzero-exit path keeps its real exit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e canonical registration

The $ST_HOOKS expansion in every generated hook command is now
double-quoted — a whitespace-containing custom or XDG root made Claude
treat only the first path segment as the executable, silently dropping
transitions — and the managed-reference recognizer reads the quoted
executable token.

st2-hook supersession was a property of EVERY arrays="union" merge, so an
unrelated union merge could delete user array elements that merely match
the managed-path heuristic. It is now the canonical Claude settings
registration's maintenance rule only; other destinations union by exact
equality alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agent-identity: unknown
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.0.3
agent-runtime: OMP 18.0.3
tooling-profile: dotfiles@f33cd9c-dirty
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agent-identity: unknown
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.0.3
agent-runtime: OMP 18.0.3
tooling-profile: dotfiles@f33cd9c-dirty
@schickling-assistant
schickling-assistant merged commit 7d6a111 into main Aug 24, 2026
1 check passed
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.

2 participants