Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions cli/src/services/hooks/claude_mutation_scope/fixtures/NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,45 @@ double-fork, or daemonizing patterns this probe did not exercise — those
remain unproven, and D20's unsupported-boundary wording stays in place
regardless, since this PR implements no detection or supervision either way,
and no static shell-command inspection is added.

## T01 managed-background lifecycle probe (Claude Code 2.1.284)

### Capture outcome

T01 was attempted on the installed Claude Code `2.1.284` on 2026-10-01. The
probe used an isolated temporary settings file containing an additive capture
hook and `--setting-sources user`; the repository `.claude/settings.json` was
not changed. The capture hook received one empty-stdin `SessionStart` event at
`2026-10-01T20:39:15.014Z`, then the non-interactive Claude session produced no
model response or tool event for more than 120 seconds and was interrupted.
A no-hook control invocation behaved the same way for 30 seconds. A `--bare`
control exited immediately because this environment's authentication is
Claude.ai OAuth, while bare mode requires an API key. The machine-readable
record is `probe18-t01-current-version.evidence.json`.

No current-version tool or lifecycle payload was fabricated from the missing
response. The 14 cases are recorded as not capturable for this run because the
session never reached a model turn: successful background Bash; non-zero exit;
cancellation/kill; another tool while active; Stop; StopFailure;
UserPromptSubmit; SubagentStop; SessionEnd; a repository mutation after the
initial acknowledgement; Claude process exit; idle-session completion;
foreground-to-background transition; and mid-session hook-registration
changes. Existing `probe14-*` and `probe17-*` fixtures remain prior-version
evidence from Claude Code `2.1.258`, not current-version T01 captures.

### G1-G7 and boundary answers

| Determination | Claude 2.1.284 result | Evidence |
|---|---|---|
| G1: acknowledgement precedes process completion and carries an identifier | unknown | No background tool call reached the hook capture; `probe14` shows this only on 2.1.258. |
| G2: later terminal event for success, failure, and cancellation | unknown | No current-version terminal event was observed. |
| G3: stable identifier joins acknowledgement and terminal event | unknown | No current-version pair was observed. |
| G4: last child write < termination <= terminal event delivery | unknown | No child write, termination, or terminal event timestamps were produced. |
| G5: lifecycle boundaries prove no further mutation | unknown for Stop, StopFailure, UserPromptSubmit, SubagentStop, SessionEnd, WorktreeRemove, and Claude process exit | None of the required current-version boundary sequences ran. |
| G6: terminal event arrives while session is idle | unknown | No background task completed. |
| G7: registration and settings-snapshot behavior | unknown | No lifecycle sequence reached the point where registration behavior could be tested. |

Under the plan, `unknown` is treated as `no`. The decision gate is therefore
**stop and revise toward SCE-owned process supervision**: the current denial
stays in place, and T02 must not proceed as an implementation of the
hook-driven lifecycle until a later probe establishes the required contract.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"probe": "T01",
"tested_at_utc": "2026-10-01",
"claude_version": "2.1.284",
"settings": "isolated temporary settings passed with --settings and --setting-sources user; the repository .claude/settings.json was not changed",
"capture_hook": "scratchpad/t01-capture/capture.sh",
"capture_directory": "/tmp/sce-claude-t01/events",
"captured_events": [
{
"hook_event_name": "SessionStart",
"captured_at": "2026-10-01T20:39:15.014Z",
"payload": "empty stdin"
}
],
"attempts": [
{
"kind": "hooked_session",
"command": "claude --print --output-format text --settings <isolated-settings> --setting-sources user --permission-mode bypassPermissions --dangerously-skip-permissions 'Reply with exactly OK and do not use any tools.'",
"result": "no model response or tool event after more than 120 seconds; interrupted with SIGINT"
},
{
"kind": "control_session_without_hooks",
"command": "claude --print --no-session-persistence --settings '{\"hooks\":{}}' --setting-sources user --permission-mode auto --max-budget-usd 0.01 'Reply with exactly OK.'",
"result": "no model response after 30 seconds; interrupted with SIGINT"
},
{
"kind": "bare_control",
"command": "claude --print --bare --no-session-persistence ...",
"result": "exited immediately with 'Not logged in'; bare mode only accepts API-key authentication in this environment"
}
],
"probe_cases": "The 14 T01 cases were not run in the current-version session because no model turn completed, so no tool or lifecycle payload could be produced. Each case is therefore recorded as not capturable for this run rather than inferred from absent events.",
"prior_evidence": {
"version": "2.1.258",
"fixture": "probe14-run-in-background-true.post_tool_use.json",
"observed": "PostToolUse acknowledged a background Bash after 8ms and carried backgroundTaskId bm2b4006o",
"status": "prior-version evidence only; not sufficient to satisfy the current-version T01 gate"
},
"decision_gate": "stop_and_revise_toward_sce_owned_process_supervision",
"reason": "The installed version did not produce the terminal lifecycle evidence required to establish G1-G4 and a proven G5 boundary. The existing background denial must remain in place."
}
9 changes: 9 additions & 0 deletions cli/src/services/hooks/claude_mutation_scope/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::services::hooks;
use crate::services::mutation_trace::runtime::resolve_git_dir;
use crate::services::observability::traits::Logger;

use super::events::BASH_TOOL_NAME;
use super::state;
use super::{
abandon_payload, classify_tool, claude_scope_close_event_id, claude_scope_start_event_id,
Expand Down Expand Up @@ -245,6 +246,10 @@ pub(super) fn handle_pre_tool_use(
return String::new();
}

if is_untracked_background_bash(execution) {
return String::new();
}

if is_explicit_background_shell(&identity.tool_name, execution.run_in_background) {
return pre_tool_use_deny_json(EXPLICIT_BACKGROUND_SHELL_DENY_REASON);
}
Expand Down Expand Up @@ -281,6 +286,10 @@ pub(super) fn handle_pre_tool_use(
}
}

fn is_untracked_background_bash(execution: &ClaudeToolExecution) -> bool {
execution.run_in_background && execution.identity.tool_name == BASH_TOOL_NAME
}

pub(super) enum BarrierOutcome {
Proceed,
Deny,
Expand Down
Loading
Loading