{
"schema": 1,
"about": "Every Claude Code behavior csift depends on, one claim per entry. Each check names the instrument that ran, what it observed and the counting rule; the README badge version is admitted only when every claim carries a check at that version. Never bulk-edit the checks.",
"verified_claude_code": "2.1.258",
"claims": [
{
"id": "BG-001",
"area": "background-tasks",
"behavior": "A backgrounded shell launch is an ordinary `type:\"assistant\"` record carrying a `Bash` (on Windows `PowerShell`) `tool_use` whose input is `{command, description?, timeout?, run_in_background:true, dangerouslyDisableSandbox?}`. Across 2942 corpus launches `description` was present on 99.4% and `timeout` on 25.0%. The flag is NOT exclusive to the two shell tools (the `Agent` tool takes it too, 257 launches) and it is NOT the only way into the background: a FOREGROUND shell command that exceeds its timeout is auto-moved to the background, minting a `backgroundTaskId` with no `run_in_background` on the launching tool_use (80 corpus cases, CC 2.1.210 through 2.1.258).",
"depends": "csift's launch ingester keys on exactly that shape (tool name `Bash` or `PowerShell` plus `input.run_in_background == true`); a renamed input flag would make every background task invisible and collapse the `idle-background-open` verdict.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "49-59",
"snippet": " Block::ToolUse {\n id: Some(id),\n name: Some(name),\n input: Some(input),\n ..\n } if (matches!(name.as_str(), \"Bash\" | \"PowerShell\")\n && input\n .get(\"run_in_background\")\n .and_then(serde_json::Value::as_bool)\n == Some(true))\n || name == \"Monitor\" =>"
},
{
"path": "src/live/background.rs",
"lines": "4-7",
"snippet": "//! - a backgrounded SHELL: a `Bash` tool_use with `input.run_in_background:true`; its\n//! tool_result arrives within milliseconds (\"Command running in background with ID:\n//! <id>. Output is being written to: <path> ...\"), so the tail state machine pairs it\n//! at once - it is invisible to the unreturned-call logic by construction;"
}
],
"instrument": "`rg -l '\"run_in_background\":\\s*true' ~/.claude/projects --glob '*.jsonl'`, then parse each matching line's `tool_use` blocks. Counting rule: one launch = one `tool_use` block with `name` in {Bash, PowerShell} and `input.run_in_background === true`, keyed by (file, tool_use id) and deduped on that key - a raw line count over-reads because the field is echoed back inside tool_result text.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc258.strings.txt ; rg -o '.{0,200}run_in_background:[^,]{0,80}' cc258.strings.txt ; rg -o '.{0,300}The PowerShell command to execute.{0,500}' cc258.strings.txt ; csift search 'run_in_background' --raw --max-count 0 | python3 (parse every tool_use block, dedupe by tool_use id) ; csift search 'was moved to the background \\(ID' --raw --max-count 0",
"observed": "Binary, the shell tool handler destructure: `let{command:we,description:Ie,timeout:Pe,run_in_background:De}=e`. Binary, the PowerShell tool input schema: `tt({command:i().refine(eU,Js).describe(\"The PowerShell command to execute\"),timeout:$M(A().optional()).describe(`Optional timeout in milliseconds (max ${We()})`),description:i().optional().describe(\"Clear, concise description of what this command does in active voice.\"),run_in_background:aA(M().optional()).describe(\"Set to true to run this command in the background.\"),dangerouslyDisableSandbox:aA(M().optional()).describe(\"Set this to true to dangerously override sandbox mode and run commands without sandboxing.\")})`. Corpus: 2942 distinct Bash launches, 0 PowerShell launches; description present on 2925 (99.4%), timeout on 735 (25.0%); input key sets 2192 {command,description,run_in_background} / 731 +timeout / 13 {command,run_in_background} / 4 {command,run_in_background,timeout} / 2 +dangerouslyDisableSandbox. A further 257 tool_use blocks carry run_in_background:true under the tool name Agent. Separately, 80 tool_result records read `... was moved to the background (ID: ...)` and 0 of their 80 launching tool_use blocks carried run_in_background at all (58 of the 80 still carry toolUseResult.backgroundTaskId); their records span versions 2.1.210 through 2.1.258, 7 of them at 2.1.258.",
"rule": "One launch = one tool_use block whose name is Bash or PowerShell and whose input.run_in_background === true, deduped by tool_use id over the whole corpus (a raw line count over-reads because the flag is echoed inside result text). Percentages are over that deduped launch set. The auto-background count is one per tool_result block whose text contains `moved to the background (ID:`, deduped by tool_use id.",
"note": "The tool-name-plus-flag shape csift keys on is intact in 2.1.258 and the code site is verbatim at src/live/background_scan.rs:49-59. Two corrections. (1) A fifth input key `dangerouslyDisableSandbox` exists in both shell schemas and appears on disk. (2) The clause 'there is no other on-disk marker for it' is wrong: an auto-backgrounded foreground command is a real background task with a real 9-char id that csift's ingester cannot see, because the ingester requires input.run_in_background == true. The PowerShell half of the claim is confirmed only from the binary schema; this machine has 0 PowerShell tool_use blocks on disk, so a Windows session would be needed to observe the record shape."
}
]
},
{
"id": "BG-002",
"area": "background-tasks",
"behavior": "The paired `tool_result` for a backgrounded shell opens `Command running in background with ID: <id>. Output is being written to: <abs path>.` and then appends, conditionally, ONE of two notify sentences (`You will be notified when it completes.` on 97.2% of corpus results, or `If it exits while you are still working you will be notified, but it is terminated when you give your final response...` on 1.0%) or neither (1.8%), then usually `To check interim output, use Read on that file path.`, then optionally a `Session cwd remains <path>; directory changes made by the backgrounded command do not apply to subsequent commands.` line (34.0%). The structured `toolUseResult` is `{stdout, stderr, interrupted, isImage, noOutputExpected, backgroundTaskId}` on 51.9% of results, that set plus `backgroundCwdHint` on 33.0%, absent entirely on 13.7% (the subagent lane), and may also carry `assistantAutoBackgrounded`, `backgroundEndsWithFinalResponse` or `dangerouslyDisableSandbox`.",
"depends": "csift lifts the task id after the literal `with ID: ` and the output path after `written to: `, falling back to `toolUseResult.backgroundTaskId` / `taskId`; a reworded result string breaks the text path and leaves the id null on the lanes where the structured field is absent.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "104-121",
"snippet": " let text = content\n .as_ref()\n .map(crate::model::tool_result_content_text)\n .unwrap_or_default();\n if task.id.is_none() {\n task.id = after_marker(&text, \"with ID: \")\n .or_else(|| after_marker(&text, \"(task \"))\n .or_else(|| {\n let v = rec.tool_use_result_value()?;\n v.get(\"backgroundTaskId\")\n .or_else(|| v.get(\"taskId\"))?\n .as_str()\n .map(str::to_string)\n });\n }\n if task.output_file.is_none() {\n task.output_file = after_marker(&text, \"written to: \");\n }"
},
{
"path": "src/live/background_scan.rs",
"lines": "165-179",
"snippet": "pub(crate) fn after_marker(text: &str, marker: &str) -> Option<String> {\n let start = text.find(marker)? + marker.len();\n let rest = &text[start..];\n let end = rest\n .char_indices()\n .find(|&(i, c)| {\n c.is_whitespace()\n || c == ','\n || c == ')'\n || (c == '.' && rest[i + 1..].starts_with([' ', '\\n']))\n })\n .map_or(rest.len(), |(i, _)| i);\n let tok = rest[..end].trim_end_matches('.');\n (!tok.is_empty()).then(|| tok.to_string())\n}"
}
],
"instrument": "Launch any command with `run_in_background` and read the result record: `csift search 'Command running in background' @<session> --raw | head -1`. Counting rule: one verbatim result string per launch; the unit test `helpers_parse_the_result_text_and_locate_the_main_transcript` in src/live/tests/background_scan.rs pins both markers.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -o '.{0,150}Command running in background with ID: \\$\\{e\\}.{0,400}' cc258.strings.txt ; rg -o 'To check interim output.{0,200}' cc258.strings.txt ; csift search 'Command running in background with ID' --raw --max-count 0 | python3 (bucket the tool_result text and the toolUseResult key set, dedupe by tool_use id)",
"observed": "Binary: the result text is assembled from three parts, `[v,C,I].filter(Boolean).join(\" \")`, where v = `Command running in background with ID: ${e}. Output is being written to: ${n}.` (or, on the auto-background path, `...id not complete within its ${...}s timeout and was moved to the background (ID: ${e}). Output is being written to: ${n}.`), C is one of `You will be notified when it completes.` / `If it exits while you are still working you will be notified, but it is terminated when you give your final response and no notification can follow that - so do not end your turn to wait for it; if you need its result, wait for it before giving your final response.` / omitted, and I = `To check interim output, use ${_} on that file path.` or omitted. Corpus, 3157 deduped launch results: 3070 carry `You will be notified when it completes.`, 31 carry the `If it exits while you are still working` variant, 56 carry neither; 3095 carry `To check interim output, use Read on that file path.`; 1072 (34.0%) carry a trailing line `Session cwd remains <path>; directory changes made by the backgrounded command do not apply to subsequent commands.`. toolUseResult key sets: 1638 (51.9%) exactly {stdout,stderr,interrupted,isImage,noOutputExpected,backgroundTaskId}; 1041 (33.0%) that set plus `backgroundCwdHint`; 434 (13.7%) NO toolUseResult field at all; 21 plus `assistantAutoBackgrounded`; 15 without `backgroundTaskId`; 5 plus `backgroundEndsWithFinalResponse`; 2 plus `dangerouslyDisableSandbox`. A live launch in this session returned the `If it exits while you are still working` variant.",
"rule": "One result per tool_result block whose text contains `Command running in background with ID`, deduped by tool_use id; text variants bucketed by which notify sentence the text contains; key sets read off the carrying record's top-level toolUseResult.",
"note": "Both marker literals csift lifts on - `with ID: ` and `written to: ` - are intact, so the text extraction path still works and src/live/background_scan.rs:104-121 and :165-179 are verbatim. What is wrong is the word 'exactly': the text now has three notify branches and an optional cwd-hint tail, and the toolUseResult key set has grown by four optional fields and is absent outright on one result in seven. The auto-background opener (`moved to the background (ID: ...)`) carries neither `with ID: ` nor `(task `, so on that path csift's id can only come from the structured fallback."
}
]
},
{
"id": "BG-003",
"area": "background-tasks",
"behavior": "A backgrounded shell's `tool_result` normally arrives sub-second (corpus median 0.42s, 90.4% under 1s) but not always: 9.6% of 2941 pairings took over 1s, 48 took over 5s and the slowest took 197.9s, and 1 launch of 2942 was never paired at all. During that window the launch IS an unreturned call. Once paired - which is the steady state for the whole life of the background command - the tail state machine reports `no pending call`, so a running background shell is invisible to unreturned-call liveness logic.",
"depends": "This is why csift added the whole-file background scan and the seventh verdict `idle-background-open`; before it, a session idling at `end_turn` with a running background build reported `idle-eot` and satisfied `wait --until stop`.",
"code": [
{
"path": "src/live/background.rs",
"lines": "4-7",
"snippet": "//! - a backgrounded SHELL: a `Bash` tool_use with `input.run_in_background:true`; its\n//! tool_result arrives within milliseconds (\"Command running in background with ID:\n//! <id>. Output is being written to: <path> ...\"), so the tail state machine pairs it\n//! at once - it is invisible to the unreturned-call logic by construction;"
},
{
"path": "src/live/verdict.rs",
"lines": "358-367",
"snippet": " } else if eot_shape && background.open_counted() > 0 {\n notes.push(\n \"the turn ended, but background task(s) have not returned - by design (a dev \\\n server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n the next session start. `--background-since` / `--ignore-background` narrow \\\n what counts; kill a dead one with the tool or the shell\"\n .to_string(),\n );\n Verdict::IdleBackgroundOpen"
}
],
"instrument": "Launch `sleep 600` with `run_in_background`, then run `csift status @<session>`: the tail row reads `no pending call; last stop_reason end_turn` while the verdict is `idle-background-open` with a shell row. Counting rule: one launch, one status read; the e2e test `an_open_background_shell_is_the_seventh_verdict_with_its_row` in tests/cli/live/background.rs pins it against a fixture.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search 'run_in_background' --raw --max-count 0 and csift search 'Command running in background with ID' --raw --max-count 0, joined in python on tool_use id, delta = result record timestamp minus launch record timestamp ; then a live check: a `sleep 45` launched with run_in_background:true followed immediately by `csift status @trap:<marker>`",
"observed": "2942 launches, 2941 paired (99.97%), 1 never paired. Deltas: min 0.049s, p50 0.416s, p90 0.965s, p99 7.397s, max 197.855s; 90.4% under 1s, 95.2% under 2s, 48 over 5s. No multi-tool_use-block confound (0 of the 2942 launches shared its assistant record with another tool_use block). Live `csift status` while the background sleep was open printed `tail unreturned Bash call (0s ago)` for the FOREGROUND csift call itself, and the background sleep appeared only in the separate section as `background 3 open; 154 completed, 1 failed, 15 killed, 0 stopped` with a `bg shell <9-char b-id> launched ... output 0 B` row.",
"rule": "One delta per (launch tool_use id, its tool_result) pair, both timestamps read off the carrying records; unpaired = a launch tool_use id that no tool_result block in the corpus names.",
"note": "The load-bearing half - a running background shell never shows up as a pending call, so it needs its own scan and its own verdict - is confirmed live: the status tail row named a different, foreground call while the open sleep was counted only under the background section. 'Within milliseconds' is the median, not the rule; the launch is genuinely unreturned for a stretch on roughly one launch in ten. src/live/background.rs:4-7 and src/live/verdict.rs:358-367 are verbatim. The live verdict was `running` rather than `idle-background-open` because the session was mid-turn, which is the documented precondition (idle-background-open needs a clean end_turn)."
}
]
},
{
"id": "BG-004",
"area": "background-tasks",
"behavior": "An async agent launch is a `tool_result` whose `toolUseResult` is `{isAsync:true, status:\"async_launched\", agentId, description, resolvedModel?, prompt, outputFile, canReadOutputFile}` (resolvedModel is optional - absent on 36 of 298 corpus acks - and a `modelsUsed` array appears when more than one model was used) and whose text opens `Async agent launched successfully.`, now followed on 66% of corpus acks by a parenthetical warning not to quote the result. The `agentId` is a 17-character `a`-led id and the completion reuses the `<task-notification>` machinery with `<task-id>` equal to it.",
"depends": "csift's launch ingester detects the launch by the sentinel `toolUseResult.status == \"async_launched\"` (falling back to the text prefix) and lifts `agentId`, `description` and `outputFile` from the structured echo; a renamed sentinel makes async agents invisible to the background section.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "124-133",
"snippet": " // An async agent launch: the sentinel status on the structured echo.\n let probe = rec.tur_probe();\n let launched = probe\n .as_ref()\n .and_then(|p| p.status.as_ref())\n .and_then(serde_json::Value::as_str)\n == Some(\"async_launched\");\n if !launched {\n continue;\n }"
},
{
"path": "src/live/background_scan.rs",
"lines": "142-156",
"snippet": " tasks.entry(tuid.clone()).or_insert(BgTask {\n kind: BgKind::Agent,\n id: s(\"agentId\"),\n tool_use_id: tuid.clone(),\n description: s(\"description\"),\n command: None,\n launched_utc: rec.timestamp.clone(),\n lane: lane.to_string(),\n output_file: s(\"outputFile\"),\n state: BgState::Open,\n returned_utc: None,\n output_bytes: None,\n output_age_secs: None,\n ignored_by: None,\n });"
},
{
"path": "src/live/background.rs",
"lines": "8-9",
"snippet": "//! - an async AGENT: a tool_result whose `toolUseResult` is `{isAsync:true,\n//! status:\"async_launched\", agentId, description, outputFile}`;"
},
{
"path": "src/model/markers.rs",
"lines": "180-185",
"snippet": "/// The leading sentence of an ASYNC/background `Agent` spawn's launch-confirmation tool_result\n/// (`\"Async agent launched successfully.\\nagentId: …\"`). This is a launch ACK, NOT the child's\n/// report - the report arrives LATER via the `<task-notification>` `<result>` pulse (G1 → inbox).\n/// On disk the ack also carries the structured `toolUseResult.{isAsync:true, status:\"async_launched\"}`\n/// shape ([`Record::is_async_launch_ack`] prefers the structured signal, falls back to this prefix).\npub const ASYNC_LAUNCH_ACK_PREFIX: &str = \"Async agent launched successfully\";"
},
{
"path": "src/subagent/spawn.rs",
"lines": "258",
"snippet": "pub(crate) const ASYNC_LAUNCH_SENTINEL: &str = \"Async agent launched\";"
}
],
"instrument": "`rg -l '\"status\":\"async_launched\"' ~/.claude/projects --glob '*.jsonl'`, then parse a matching line's `toolUseResult` (each hit must also carry `agentId` and `outputFile`). Counting rule: one launch per (file, tool_use id) whose `toolUseResult.status == \"async_launched\"`; the unit test `async_agent_launches_and_the_stopped_notice` in src/live/tests/background_scan.rs pins the sentinel.",
"located": {
"claude_code": "2.1.252",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; src/model/markers.rs ASYNC_LAUNCH_ACK_PREFIX doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -o '.{0,180}async_launched.{0,180}' cc258.strings.txt ; csift search 'Async agent launched successfully' --raw --max-count 0 | python3 (bucket toolUseResult key sets, agentId length and first character, first line of the ack text, dedupe by tool_use id)",
"observed": "Binary output schema: `n=c({status:x(\"async_launched\"),isAsync:x(!0).optional(),agentId:i().describe(\"The ID of the async agent\"),description:i().describe(\"The description of the task\"),resolvedModel:i().optional().describe(\"Model i...`; binary runtime object: `{data:{isAsync:!0,status:\"async_launched\",agentId:js.agentId,description:r,resolvedModel:Rn,prompt:e,outputFile:pl(js.agentId),canReadOutputFile:si}}` and a second site `{data:{isAsync:!0,status:\"async_launched\",agentId:xs,description:r,resolvedModel:ui,...sr.length>1&&{modelsUsed:[...sr]},prompt:e,outputFile:pl(xs),canReadOutputFile:$g}}`. Corpus: 298 deduped acks; agentId length 17 on 298 of 298 and first character `a` on 298 of 298; toolUseResult key sets 262 = {agentId,canReadOutputFile,description,isAsync,outputFile,prompt,resolvedModel,status} and 36 = the same minus `resolvedModel`. First line of the ack text: 196 read `Async agent launched successfully. (This tool result is internal metadata - never quote or paste any part of it, including the agentId below, into a user-facing reply.)` and 102 read `Async agent launched successfully.`. In the classified notification corpus 183 sections carry a 17-character `a`-led `<task-id>`.",
"rule": "One ack per tool_result block whose text starts with `Async agent launched successfully`, deduped by tool_use id; key sets and agentId shape read off the carrying record's toolUseResult; the 17-char task-id count is one per `<task-notification>` section in records csift classifies harness.notification.*.",
"note": "The sentinel csift keys on - toolUseResult.status == 'async_launched' - and the text-prefix fallback are both intact, and all five code sites are verbatim. Two corrections: `resolvedModel` is optional in the schema and missing on 12% of corpus acks, and a ninth key `modelsUsed` exists in the binary for the multi-model case; and the ack's first line is no longer just the one sentence."
}
]
},
{
"id": "BG-005",
"area": "background-tasks",
"behavior": "The async launch ack is NOT the child's report: the child's report arrives LATER as a `<task-notification>` carrying a `<result>` body, joined back to its launching spawn through the pulse's embedded `<tool-use-id>`; a bare launch-ack pulse with no `<result>` carries no communication direction at all.",
"depends": "csift labels the ack `agent.tool.result` only (never a child return) and emits an extra `agent.communication.inbox` view (child to self) for a result-bearing pulse, excerpting the `<result>` body; conflating the two attributes the child's work to the launch instant, and without the id join the report is attributed to nobody and degrades to `?`.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "70-81",
"snippet": " /// True when this carrier is an ASYNC-LAUNCH ACK, not a child return (smoke-found bug): an\n /// ASYNC/background `Agent` spawn's tool_result is the immediate launch confirmation\n /// (`\"Async agent launched successfully.\\nagentId: …\"`), shaped on disk as\n /// `toolUseResult.{isAsync:true, status:\"async_launched\"}`. It shares the spawn\n /// `tool_use_id`, so the [`SpawnLookup`] WOULD resolve it - but it is the LAUNCH ack, not\n /// the work product. The async child's real report arrives LATER via the\n /// `<task-notification>` `<result>` pulse (G1 → `agent.communication.inbox`), never via this\n /// tool_result. So a launch ack is `agent.tool.result` ONLY (unlike a SYNC one-shot Task\n /// return, which IS the child's reply → `…inbox`). Robust dual detection: the structured\n /// `toolUseResult` shape first, then the content prefix ([`ASYNC_LAUNCH_ACK_PREFIX`]) for a\n /// record lacking the structured field.\n pub(crate) fn is_async_launch_ack(&self) -> bool {"
},
{
"path": "src/model/classify.rs",
"lines": "461-464",
"snippet": " if section.contains(NOTIFICATION_RESULT_TAG) {\n let child = extract_xml_tag(section, \"tool-use-id\")\n .and_then(|id| ctx.spawn.and_then(|sp| sp.child_for_spawn_tool_use_id(&id)))\n .unwrap_or_else(|| \"?\".to_string());"
},
{
"path": "src/model/markers.rs",
"lines": "180-185",
"snippet": "/// The leading sentence of an ASYNC/background `Agent` spawn's launch-confirmation tool_result\n/// (`\"Async agent launched successfully.\\nagentId: …\"`). This is a launch ACK, NOT the child's\n/// report - the report arrives LATER via the `<task-notification>` `<result>` pulse (G1 → inbox).\n/// On disk the ack also carries the structured `toolUseResult.{isAsync:true, status:\"async_launched\"}`\n/// shape ([`Record::is_async_launch_ack`] prefers the structured signal, falls back to this prefix).\npub const ASYNC_LAUNCH_ACK_PREFIX: &str = \"Async agent launched successfully\";"
}
],
"instrument": "`rg -o '<task-notification>.*?</task-notification>' <transcript> | rg -c '<result>'` against the total notification count. Counting rule: one observation per notification section, bucketed by whether it carries a `<result>` tag.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/live/background.rs module doc; src/model/classify.rs comment; src/model/markers.rs ASYNC_LAUNCH_ACK_PREFIX doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search '' -t harness.notification --raw --max-count 0 (2698 records) and csift search 'Async agent launched successfully' --raw --max-count 0, joined in python: every `<task-notification>` section indexed by its `<tool-use-id>`, then matched against each ack's tool_use id, bucketed by `<result>` presence and by timestamp order",
"observed": "298 async launch acks; 155 of them have at least one notification section joined by `<tool-use-id>`; 155 of 155 of those sections carry a `<result>` tag; 155 of 155 have the notification timestamp strictly LATER than the ack's, gap min 78.3s, median 530.5s, max 5101.2s. Across all 1849 distinct tool-use-id-bearing notification sections only 322 carry `<result>` - shell and monitor completions do not.",
"rule": "One join per (ack tool_use id, notification section naming that id); `<result>` presence by literal tag search inside the section; ordering by the two carrying records' timestamps. The 143 acks with no notification are agents that never reported before the session ended, or whose pulse landed on a line csift does not classify.",
"note": "Both halves confirmed with no correction needed: the report is never the ack (100% of joined notifications are strictly later, by 78s to 85 minutes), the join key is the launching tool_use id, and result-bearing sections are a minority (322 of 1849) so a non-result pulse genuinely has nothing to attribute. src/model/classify.rs:70-81 and :461-464 and src/model/markers.rs:167-172 are verbatim."
}
]
},
{
"id": "BG-006",
"area": "background-tasks",
"behavior": "The `Monitor` tool's input schema requires `description` and takes optional `timeout_ms` (integer, minimum 1000, default 300000, maximum 3600000, ignored when persistent) and `persistent` (boolean, default false), plus EXACTLY ONE of `command` (each stdout line is an event) or `ws:{url, protocols?}` (ws:// or wss:// only); remote sessions cap the timeout at 1800000 and force `persistent:false`.",
"depends": "csift models a Monitor arm as a background task of kind `monitor` and names it by `input.command` or, for a websocket monitor, by `input.ws.url`; a monitor with neither field would render nameless.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "61-74",
"snippet": " let get = |k: &str| {\n input\n .get(k)\n .and_then(serde_json::Value::as_str)\n .map(str::to_string)\n };\n // A websocket monitor has no command: its url is the thing to name.\n let command = get(\"command\").or_else(|| {\n input\n .get(\"ws\")\n .and_then(|w| w.get(\"url\"))\n .and_then(serde_json::Value::as_str)\n .map(str::to_string)\n });"
}
],
"instrument": "Read the Monitor tool's live input schema from the harness tool listing, then census the corpus: `rg -c '\"name\":\"Monitor\"' ~/.claude/projects --glob '*.jsonl'` and enumerate the key sets of the matching `tool_use` inputs. Counting rule: one key-set observation per `tool_use` block whose `name` is `Monitor`.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -o 'var H=3600000,B=1800000,j=300000;.{0,2200}' cc258.strings.txt ; csift search 'Monitor' -t agent.tool.use --raw --max-count 0 | python3 (census the input key sets of every tool_use block named Monitor)",
"observed": "Binary, verbatim: `var H=3600000,B=1800000,j=300000;` then `function ie(){return{description:i().describe(\"Short human-readable description of what you are monitoring (shown in notifications).\"),timeout_ms:A().min(1000).optional().default(j).describe(`Kill the monitor after this deadline. Default ${j}ms, max ${H}ms. Ignored when persistent is true.`),persistent:M().optional().default(!1).describe(\"Run for the lifetime of the session (no timeout). Use for session-length watches like PR monitoring or log tails. Stop with TaskStop.\")}}`, the ws sub-schema `se=()=>c({url:i().refine(...).refine((e)=>{...return(o.protocol===\"ws:\"||o.protocol===\"wss:\")&&!o.username&&!o.password...},\"url must be a valid ASCII ws:// or wss:// URL with no userinfo or whitespace\"),protocols:R(i().regex(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/,\"protocol must be an RFC 6455 token\")).refine(...).optional()})`, the assembly `ue=m(()=>tt({...ie(),command:re().optional().describe(oe),ws:se().optional()}).refine((e)=>le(e.command,e.ws),\"exactly one of command or ws\").refine(ce,ae))` with `ae={message:\"timeout_ms must be <= ${H}\",path:[\"timeout_ms\"]}` and `ce(e){return e.persistent||e.timeout_ms<=H}`, and the remote clamp `function yjn(e){if(!a.CLAUDE_CODE_REMOTE)return{timeout_ms:e.timeout_ms,persistent:e.persistent};return{timeout_ms:e.persistent?B:Math.min(e.timeout_ms,B),persistent:!1}}`. Corpus: 434 tool_use blocks named Monitor, input key sets 405 {command,description,persistent,timeout_ms}, 25 {command,description,persistent}, and 4 blocks whose keys match no schema field; 0 `ws` monitors.",
"rule": "Schema read verbatim off the 2.1.258 binary; one key-set observation per tool_use block whose name is Monitor, deduped by tool_use id.",
"note": "Every element of the claim reads back verbatim from 2.1.258: description required, timeout_ms integer min 1000 default 300000 max 3600000 and ignored when persistent, persistent boolean default false, exactly one of command or ws, ws restricted to ws:// or wss:// with an optional RFC 6455 protocols array, and the remote clamp to 1800000 with persistent forced false. The websocket form is unexercised on this machine (0 of 434 arms), so the ws branch is confirmed from the schema only, not from a record."
}
]
},
{
"id": "BG-007",
"area": "background-tasks",
"behavior": "A schema-valid `Monitor` arm is always immediately paired on disk (430 of 430 corpus arms whose input matches the schema had a `Monitor started` result; 4 further Monitor tool_use blocks with inputs matching no schema field got no result at all). The result text has two forms, `Monitor started (task <id>, timeout <N>ms). ...` and `Monitor started (task <id>, persistent - runs until TaskStop or session end). ...`, each continuing `You will be notified on each event. Keep working - do not poll or sleep. Events may arrive while you are waiting for the user - an event is not their reply.`. `toolUseResult` is exactly `{taskId, timeoutMs, persistent}` (absent on the subagent lane, 3 of 430), with `timeoutMs` 0 when `persistent` is true.",
"depends": "csift extracts the monitor id after the literal `(task ` and falls back to `toolUseResult.taskId`, so an armed monitor never appears as an unreturned call and is visible only through the background section.",
"code": [
{
"path": "src/live/background.rs",
"lines": "10-17",
"snippet": "//! - a MONITOR: the `Monitor` tool_use (a command whose stdout lines are events, or a\n//! websocket), armed on disk as an immediately-paired pair whose result reads `Monitor\n//! started (task <id>, …)` with `toolUseResult.taskId`. It shares the `b…` id namespace\n//! with backgrounded shells (both are `local_bash` tasks in the harness), so only the\n//! tool name tells them apart. Event pulses (`Monitor event: …`, no `<status>`) never\n//! close it; a termination notice (`<status>completed</status>`, summary opening\n//! `Monitor`) or a timeout event does; a PERSISTENT monitor never returns by design\n//! (measured: 30% of armed monitors produced no notification at all)."
},
{
"path": "src/live/background_scan.rs",
"lines": "100-111",
"snippet": " if let Some(task) = tasks.get_mut(tuid) {\n // The shell result: the task id + output path live in the text (and\n // the id also in `toolUseResult.backgroundTaskId`); a Monitor arm reads\n // `Monitor started (task <id>, …)` with `toolUseResult.taskId`.\n let text = content\n .as_ref()\n .map(crate::model::tool_result_content_text)\n .unwrap_or_default();\n if task.id.is_none() {\n task.id = after_marker(&text, \"with ID: \")\n .or_else(|| after_marker(&text, \"(task \"))\n .or_else(|| {"
}
],
"instrument": "`rg -o 'Monitor started \\(task [a-z0-9]{9}' ~/.claude/projects --glob '*.jsonl' | wc -l` against `rg -c '\"name\":\"Monitor\"'`, then join each arm's `tool_use` id to a `tool_result` in the same file. Counting rule: one arm per `tool_use` block; an arm is unreturned when no `tool_result` block in the same file carries its id.",
"located": {
"claude_code": "2.1.252",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6.13; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -o '.{0,250}3600000.{0,250}' cc258.strings.txt ; csift search 'Monitor' -t agent.tool.use --raw --max-count 0 and csift search 'Monitor started' --raw --max-count 0, joined in python on tool_use id",
"observed": "Binary, verbatim: `return{data:{taskId:r,timeoutMs:T?0:g,persistent:T}}`. Corpus: 434 Monitor tool_use blocks, 430 paired with a `Monitor started` result, 4 unreturned - and all 4 of those have inputs that match no schema field (key sets `()`, `('agents',)`, `('max_results','query')`, and one using `timeout` instead of `timeout_ms`); among the 430 schema-shaped arms the pairing is 430 of 430. Result text has exactly two forms: 274 `Monitor started (task <id>, persistent - runs until TaskStop or session end). You will be notified on each event. Keep working - do not poll or sleep. Events may arrive while you are waiting for the user - an event is not their reply.` and 156 `Monitor started (task <id>, timeout <N>ms). You will be notified on each event. Keep working - do not poll or sleep. Events may arrive while you are waiting for the user - an event is not their reply.`. toolUseResult is exactly {persistent, taskId, timeoutMs} on 427 and absent on 3 (subagent lane). persistent true implied timeoutMs 0 on 274 of 274; the largest non-persistent timeoutMs observed was 3600000.",
"rule": "One arm per tool_use block named Monitor deduped by tool_use id; an arm is paired when some tool_result block in the corpus names its id AND that result's text starts with `Monitor started`; unreturned means no tool_result at all names the id.",
"note": "Three corrections. The 'none unreturned' absolute is refuted at 4 of 434, though every one of the four is a malformed call rather than a real arm, so the operational claim - a well-formed monitor never shows up as a pending call - stands. The quoted result text is missing its final sentence about events not being the user's reply, and the persistent branch has its own wording with no `timeout <N>ms` at all. The `timeoutMs` 0-when-persistent rule reads back verbatim from the binary. src/live/background.rs:10-17 and src/live/background_scan.rs:100-111 are verbatim."
}
]
},
{
"id": "BG-008",
"area": "background-tasks",
"behavior": "A command Monitor is a `local_bash` task in the harness, so its task id is drawn from the SAME `b`-prefixed namespace as a backgrounded shell's; only the tool NAME distinguishes a monitor from a shell.",
"depends": "csift classifies a background row's kind from the tool name, never from the id shape; an id-prefix classifier would mislabel every backgrounded shell as a monitor and merge the two into one id space.",
"code": [
{
"path": "src/live/background.rs",
"lines": "10-17",
"snippet": "//! - a MONITOR: the `Monitor` tool_use (a command whose stdout lines are events, or a\n//! websocket), armed on disk as an immediately-paired pair whose result reads `Monitor\n//! started (task <id>, …)` with `toolUseResult.taskId`. It shares the `b…` id namespace\n//! with backgrounded shells (both are `local_bash` tasks in the harness), so only the\n//! tool name tells them apart. Event pulses (`Monitor event: …`, no `<status>`) never\n//! close it; a termination notice (`<status>completed</status>`, summary opening\n//! `Monitor`) or a timeout event does; a PERSISTENT monitor never returns by design\n//! (measured: 30% of armed monitors produced no notification at all)."
},
{
"path": "src/live/background_scan.rs",
"lines": "75-80",
"snippet": " tasks.entry(id.clone()).or_insert(BgTask {\n kind: if name == \"Monitor\" {\n BgKind::Monitor\n } else {\n BgKind::Shell\n },"
}
],
"instrument": "`rg -o 'Monitor started \\(task [a-z0-9]+' ~/.claude/projects --glob '*.jsonl' | sort -u` against `rg -o '\"backgroundTaskId\":\"[a-z0-9]+\"' | sort -u`: both sets start with `b`. Counting rule: distinct ids on each side after dedupe.",
"located": {
"claude_code": "2.1.252",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6.13; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; CHANGELOG 0.10.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -o '.{0,120}local_bash.{0,320}' cc258.strings.txt ; csift search 'Monitor started' --raw --max-count 0 and csift search 'Command running in background with ID' --raw --max-count 0 | python3 (collect toolUseResult.taskId and toolUseResult.backgroundTaskId, dedupe, compare length, first character and overlap)",
"observed": "Binary: `var pi={local_bash:\"b\",local_agent:\"a\",remote_agent:\"r\",in_process_teammate:\"t\",local_workflow:\"w\",monitor_mcp:\"m\",monitor_ws:\"s\",mcp_task:\"k\",dream:\"d\",auto_mode_scan:\"e\"}` - a command monitor has no dedicated prefix, while the two MCP/websocket monitor kinds do (`m`, `s`). Corpus: 427 distinct monitor taskIds, all 9 characters and all first-character `b`; 2707 distinct shell backgroundTaskIds, all 9 characters and all first-character `b`; the two sets overlap in 0 ids.",
"rule": "Distinct ids after dedupe on each side; a shared namespace is evidenced by identical length and prefix with disjoint draws, since a per-kind prefix would separate them.",
"note": "Confirmed from both ends. The binary's prefix map has entries for monitor_mcp (`m`) and monitor_ws (`s`) but none for a command monitor, and on disk every one of 427 command-monitor task ids is `b`-led and 9 characters - the same shape and the same namespace as the 2707 backgrounded-shell ids, with zero collisions. So an id-prefix classifier really cannot tell a command monitor from a backgrounded shell, and csift's use of the tool name is the only sound discriminator. A websocket monitor would fall in the `s` namespace instead, but none exist on this machine."
}
]
},
{
"id": "BG-009",
"area": "background-tasks",
"behavior": "A harness background-task id is exactly 9 characters - one kind-prefix character plus 8 drawn from `0123456789abcdefghijklmnopqrstuvwxyz` - with the prefix map `local_bash:\"b\"`, `local_agent:\"a\"`, `remote_agent:\"r\"`, `in_process_teammate:\"t\"`, `local_workflow:\"w\"`, `monitor_mcp:\"m\"`, `monitor_ws:\"s\"`, `mcp_task:\"k\"`, `dream:\"d\"`, `auto_mode_scan:\"e\"`, fallback `\"x\"`.",
"depends": "csift never parses the prefix to decide a task's kind - it uses the tool name - precisely because a command Monitor is a `local_bash` task and shares the `b` namespace with backgrounded shells.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "75-80",
"snippet": " tasks.entry(id.clone()).or_insert(BgTask {\n kind: if name == \"Monitor\" {\n BgKind::Monitor\n } else {\n BgKind::Shell\n },"
},
{
"path": "src/live/background.rs",
"lines": "10-17",
"snippet": "//! - a MONITOR: the `Monitor` tool_use (a command whose stdout lines are events, or a\n//! websocket), armed on disk as an immediately-paired pair whose result reads `Monitor\n//! started (task <id>, …)` with `toolUseResult.taskId`. It shares the `b…` id namespace\n//! with backgrounded shells (both are `local_bash` tasks in the harness), so only the\n//! tool name tells them apart. Event pulses (`Monitor event: …`, no `<status>`) never\n//! close it; a termination notice (`<status>completed</status>`, summary opening\n//! `Monitor`) or a timeout event does; a PERSISTENT monitor never returns by design\n//! (measured: 30% of armed monitors produced no notification at all)."
}
],
"instrument": "`rg -o '<task-id>[a-z0-9]{9}</task-id>' ~/.claude/projects --glob '*.jsonl' | sed 's/.*>\\(.\\).*/\\1/' | sort | uniq -c` - only prefix characters from the map appear. Counting rule: one observation per extracted 9-character id; ids of other lengths (the 17-character `a`-led agent id) are a different namespace and must be excluded.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -o '.{0,120}local_bash.{0,320}' cc258.strings.txt ; csift search '' -t harness.notification --raw --max-count 0 | python3 (extract every <task-id>, bucket by length and first character) ; plus one live background launch in this session",
"observed": "Binary, verbatim: `var pi={local_bash:\"b\",local_agent:\"a\",remote_agent:\"r\",in_process_teammate:\"t\",local_workflow:\"w\",monitor_mcp:\"m\",monitor_ws:\"s\",mcp_task:\"k\",dream:\"d\",auto_mode_scan:\"e\"},yt=\"0123456789abcdefghijklmnopqrstuvwxyz\";function Yh(h){let x=pi[h]??\"x\",I=ci(8),O=x;for(let W=0;W<8;W++)O+=yt[I[W]%yt.length];re...` - one prefix character from the map (fallback `\"x\"`) plus 8 characters drawn from that 36-character alphabet, so exactly 9. Corpus: of 2692 classified notification sections, 2510 carry a 9-character `<task-id>` (2369 prefix `b`, 141 prefix `w`) and 183 carry a 17-character `a`-led id (the async agentId, a different namespace). A live background launch in this session minted a 9-character `b`-led id.",
"rule": "One observation per `<task-id>` extracted from a `<task-notification>` section in a record csift classifies harness.notification.*, bucketed by length then first character; 17-character `a`-led ids are excluded as agentIds, per the claim's own rule.",
"note": "The prefix map, the alphabet, the `x` fallback and the 1+8 = 9 length all read back verbatim from 2.1.258 - this is the strongest-evidenced claim in the set. Only two of the ten kinds are exercised on disk here (`b` local_bash 2369, `w` local_workflow 141); the other eight prefixes are confirmed from the map alone. Note that `local_agent`'s `a` prefix yields a 9-character TASK id, which is a different thing from the 17-character `a`-led agentId, and no 9-character `a`-led id appears in this corpus."
}
]
},
{
"id": "BG-010",
"area": "background-tasks",
"behavior": "A background task's completion is a `<task-notification>` whose inner tags are `<task-id>`, `<tool-use-id>`, `<output-file>`, `<status>` and `<summary>`, optionally plus `<usage>`, `<result>`, `<note>`, `<diagnostics>`, `<failures>` or `<recovery>`; a Monitor EVENT pulse is a different, three-tag shape `{event, summary, task-id}` with no `<tool-use-id>` and no `<status>` (838 of 2692 corpus sections). The `<tool-use-id>` equals the LAUNCHING tool_use id exactly - 1665 of 1849 corpus sections resolved to a launch still on disk, and none disagreed.",
"depends": "csift keys its task map by the launching tool_use id and resolves carriers by `<tool-use-id>` first and any `<task-id>` second, latest carrier wins; without the tool_use id join a task launched from a subagent lane cannot be matched to its completion at all.",
"code": [
{
"path": "src/live/background.rs",
"lines": "19-21",
"snippet": "//! Completion is a `<task-notification>` whose `<tool-use-id>` equals the launching\n//! tool_use id (an exact join; the 9-char `backgroundTaskId` is a second key, absent on\n//! 43% of subagent-lane launches), with `<status>` completed | failed | killed | stopped."
},
{
"path": "src/live/background_scan.rs",
"lines": "205-216",
"snippet": " for section in text.split(TASK_NOTIFICATION_PREFIX).skip(1) {\n let task_ids = all_xml_tags(section, \"task-id\");\n let orphan = task_ids.iter().any(|t| t.starts_with(\"__orphan_summary__\"));\n carriers.push(Carrier {\n task_ids,\n tool_use_id: extract_xml_tag(section, \"tool-use-id\"),\n status: extract_xml_tag(section, \"status\"),\n event: extract_xml_tag(section, \"event\"),\n ts: rec.timestamp.clone(),\n orphan_summary: orphan,\n });\n }"
},
{
"path": "src/live/background_scan.rs",
"lines": "279-291",
"snippet": "/// Join carriers to launches: `<tool-use-id>` first (exact), any `<task-id>` second.\n/// The latest carrier wins (an agent notifies again after a resume).\npub(crate) fn resolve_carriers(\n tasks: &mut BTreeMap<String, BgTask>,\n carriers: &[Carrier],\n notes: &mut Vec<String>,\n) {\n let mut by_id: BTreeMap<String, String> = BTreeMap::new();\n for (tuid, t) in tasks.iter() {\n if let Some(id) = &t.id {\n by_id.insert(id.clone(), tuid.clone());\n }\n }"
}
],
"instrument": "For each background launch, search the main transcript for its launching tool_use id and read the notification section that names it: `csift search '<tool_use_id>' @<session>`; compare the resolved count with `csift status @<session> --format json | jq '.background'`. Counting rule: one completion per launching tool_use id, latest carrier wins.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "A live background launch in this session, then reading the `<task-notification>` it produced and comparing its `<tool-use-id>` with the launching tool_use id read back off disk via csift search '<the launch description>' @trap:<marker> --raw ; then corpus-wide: csift search '' -t harness.notification --raw --max-count 0 | python3 (tag frequency per section, and join every section's <tool-use-id> against the launch ids collected for background shells, async acks and monitor arms)",
"observed": "Live: the notification's `<tool-use-id>` was byte-identical to the launching Bash tool_use id, and its inner tags were exactly `<task-id> <tool-use-id> <output-file> <status> <summary>`. Corpus: 2692 notification sections in 2698 records; tag frequencies task-id 2692, summary 2692, status 1854, tool-use-id 1849, output-file 1849, event 838, usage 323, result 322, note 126, diagnostics 65, failures 26, recovery 3 - the claimed five-tag set is exactly 1523 sections (56.6%), and 838 sections are event pulses carrying only {event, summary, task-id}. Join: 1849 distinct `<tool-use-id>`s, of which 1665 matched a launch found on disk (1400 background shells, 155 async acks, 110 monitor arms); the 184 unmatched carry task-ids that are 141 workflow (`w`), 27 async agent (17-char `a`) and 16 shell (`b`) whose launching records were not in scope. No `<tool-use-id>` was observed to disagree with the launch it named.",
"rule": "One section per `<task-notification>` block in a record csift classifies harness.notification.*, deduped by record uuid; a tag counts once per section; a join succeeds when the section's `<tool-use-id>` string equals a collected launch tool_use id exactly.",
"note": "The join key is confirmed the strongest way available - a launch made in this session and matched against the notification it produced minutes later - and at corpus scale nothing contradicts it. The tag list needed widening: six further tags appear (usage and result on about 12% of sections each), and the claim's five-tag shape is only 56.6% of sections because Monitor event pulses are a distinct three-tag form. The '2502 of 2502' figure is not reproducible against this corpus and has been replaced by a stated denominator; the 184 unresolved ids are workflow tasks and launches whose transcripts are out of scope, not join failures. src/live/background.rs:19-21 and src/live/background_scan.rs:215-226 and :249-261 are verbatim."
}
]
},
{
"id": "BG-011",
"area": "background-tasks",
"behavior": "The terminal `<status>` values form a small OPEN set: `completed`, `failed` (its summary ends `failed with exit code N`), `killed` (summary `Background command \"X\" was stopped`) and `stopped`.",
"depends": "csift maps those four literals onto its background state enum with `completed` as the fallback, so a NEW status value would silently be booked as completed rather than surfaced.",
"code": [
{
"path": "src/live/background.rs",
"lines": "44-74",
"snippet": "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum BgState {\n /// Launched, no completion carrier names it yet.\n Open,\n Completed,\n Failed,\n Killed,\n /// Claude Code's own orphan reconciliation at the next session start, or an\n /// explicit `stopped` status.\n Stopped,\n /// A Monitor whose timeout fired (the `[Monitor timed out …]` event).\n TimedOut,\n}"
},
{
"path": "src/live/background.rs",
"lines": "89-96",
"snippet": " pub(crate) fn from_status(status: Option<&str>) -> Self {\n match status {\n Some(\"failed\") => BgState::Failed,\n Some(\"killed\") => BgState::Killed,\n Some(\"stopped\") => BgState::Stopped,\n _ => BgState::Completed,\n }\n }"
}
],
"instrument": "The ledger's `rg -o '<status>[a-z]*</status>' ~/.claude/projects | sort | uniq -c` over-reads. Run over a single project directory it returns a FIFTH value, <status>running</status> x8, and every one of those is a csift unit-test fixture quoted inside a tool_result or an assistant message in a dev transcript. Parse the three carrier fields (a type:\"user\" string content, a queue-operation content, a queued_command attachment prompt) instead of grepping raw bytes.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md section 6 v0.10.0 ledger; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 bgverify6.py / bgverify7.py - the same walk restricted to type:\"user\" string records, censusing <status>, <summary> shape and <task-id> length per notification section; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'function ka\\('",
"observed": "Delivered type:\"user\" notification sections corpus-wide: <status>completed</status> 1808, failed 83, killed 80, stopped 5 - no fifth value (all three carriers together: completed 8693, failed 412, killed 195, stopped 10). Summary shapes per status: 79 of 83 failed end \"failed with exit code N\"; 78 of 80 killed end '\" was stopped'; 1366 completed end \"completed (exit code N)\", 132 are 'Agent \"X\" finished', 111 open with \"Monitor\". Binary: function ka({taskId:e,toolUseId:n,taskType:r,outputFile:o,status:d,summary:f,body:_,trailing:v}){let C=[[Use,e],[jdr,n],[qBe,r],[Gdr,o],[zx,d],[j0,f]] - the emitter takes status as a free-form string and writes the tag only when it is truthy, so the set is genuinely open rather than a typed enum.",
"rule": "One observation per <task-notification> section inside a delivered type:\"user\" string record, so the queue-operation and attachment echoes of the same delivery are not counted three times. A section with no <task-id> is prose that merely quotes the marker and is dropped.",
"note": "The behavior text needed no change: four values, and open by construction. The corpus cannot distinguish an open set from a closed one, but the binary can, and it shows a free-string status - which is what makes csift's `_ => BgState::Completed` fallback in from_status a silent-mislabel risk rather than a safe default. Both code snippets (src/live/background.rs:62-74 and 89-96) are verbatim in the current file."
}
]
},
{
"id": "BG-012",
"area": "background-tasks",
"behavior": "toolUseResult.backgroundTaskId is a 9-character SECOND key, never the primary join key: it was present on 2533 of the 2533 main-lane launches that have a result record (one further main-lane launch never returned a result), but on only 350 of 624 subagent-lane launches (274 carry none, 43.9%).",
"depends": "csift joins on `<tool-use-id>` first and any `<task-id>` second; making `backgroundTaskId` primary would strand 43% of subagent-lane launches as permanently open.",
"code": [
{
"path": "src/live/background.rs",
"lines": "19-21",
"snippet": "//! Completion is a `<task-notification>` whose `<tool-use-id>` equals the launching\n//! tool_use id (an exact join; the 9-char `backgroundTaskId` is a second key, absent on\n//! 43% of subagent-lane launches), with `<status>` completed | failed | killed | stopped."
},
{
"path": "src/live/background_scan.rs",
"lines": "108-117",
"snippet": " if task.id.is_none() {\n task.id = after_marker(&text, \"with ID: \")\n .or_else(|| after_marker(&text, \"(task \"))\n .or_else(|| {\n let v = rec.tool_use_result_value()?;\n v.get(\"backgroundTaskId\")\n .or_else(|| v.get(\"taskId\"))?\n .as_str()\n .map(str::to_string)\n });"
}
],
"instrument": "For every background launch, parse `toolUseResult` on the paired result and test for a `backgroundTaskId` key, bucketing by whether the file path contains a `subagents` component. Counting rule: one presence/absence observation per launch keyed (file, tool_use id).",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 bgverify2.py - a walk of ~/.claude/projects/**/*.jsonl that json-parses only lines carrying one of the byte needles run_in_background | 'Command running in background' | async_launched | task-notification | 'Monitor started' | '\"Monitor\"' | queued_command | backgroundTaskId, ingests launches exactly as src/live/background_scan.rs ingest_launches does, and reads notifications ONLY from the three carrier fields ingest_carriers reads; python3 bgverify6.py / bgverify7.py - the same walk restricted to type:\"user\" string records, censusing <status>, <summary> shape and <task-id> length per notification section",
"observed": "Main lane: 2534 backgrounded-shell launches; 2533 of them have a paired tool_result on disk and 2533 of those 2533 carry toolUseResult.backgroundTaskId (the single exception never returned a result at all, so it carries no key rather than lacking one). Subagent lane: 624 launches, all with a result on disk - 350 carry backgroundTaskId, 274 do not (43.9%). Task-id length census over delivered notifications: 9 characters x2626, 17 x192, 42 x2.",
"rule": "One presence/absence observation per launch keyed (file, tool_use id). A launch is subagent-lane iff its file path has a `subagents` component. Presence = the literal key backgroundTaskId in the paired tool_result record's toolUseResult object; the 9-character family is the shell/monitor id namespace, 17 characters is the async-agent id.",
"note": "Ratio unchanged, denominators grew with the corpus. The asymmetry is real and reproduces: every main-lane shell result that exists carries the key, while nearly half of subagent-lane ones do not, so a backgroundTaskId-primary join would strand them. Both code snippets are verbatim (src/live/background.rs:19-21 module doc, src/live/background_scan.rs:108-117)."
}
]
},
{
"id": "BG-013",
"area": "background-tasks",
"behavior": "...1015 of 2520 returned main-lane shells (40.3%) never produce a user record. The queue pair is `operation:\"enqueue\"` (no reason) followed by `operation:\"remove\"` carrying a reason: `absorbed_mid_turn` when the notification lands in a turn already in flight, `delivered_to_agent` when it is handed to an idle subagent lane (a further 2145 removes carry no reason).",
"depends": "csift's carrier ingester reads all three carrier shapes; restricting it to `type:\"user\"` records loses 40% of shell completions, leaving them permanently `open` and flipping the `idle-background-open` verdict.",
"code": [
{
"path": "src/live/background.rs",
"lines": "22-27",
"snippet": "//! It rides THREE carriers: a `type:\"user\"` string record when the session was idle,\n//! or (40% of returned shells) a `queue-operation` enqueue + remove and a\n//! `queued_command` attachment when it landed mid-turn - never a user record. A shell\n//! launched from a SUBAGENT lane is completed in the PARENT main transcript (607/618\n//! measured; zero notifications exist in any subagent transcript), so this scan reads\n//! launches from every lane and completions from the main file."
},
{
"path": "src/live/background_scan.rs",
"lines": "225-236",
"snippet": "pub(crate) fn carrier_text(rec: &Record) -> Option<String> {\n if rec.is_type(\"queue-operation\") {\n rec.content_str().map(str::to_string)\n } else if rec.attachment_type().as_deref() == Some(\"queued_command\") {\n rec.attachment_value()\n .and_then(|v| v.get(\"prompt\")?.as_str().map(str::to_string))\n } else if let Some(Content::Text(s)) = rec.message.as_ref().and_then(|m| m.content.as_ref()) {\n Some(s.clone())\n } else {\n None\n }\n}"
}
],
"instrument": "Take a known background task id from a launch result and `rg -n '<that id>' <the main transcript>`, then classify each matching line by its top-level `type`. Counting rule: one carrier observation per matching line, bucketed into `queue-operation` / `attachment` / `user`; a launch with no `user` row but a `queue-operation` row is a mid-turn delivery.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type; plus a live probe: arm a non-persistent Monitor from inside a subagent lane at CC 2.1.258, then `rg -c '<task-id>' <the subagent transcript>` against `rg -c '<task-id>' <the parent main transcript>` and json-decode each matching line",
"observed": "Notification sections by carrier field corpus-wide: queue-operation 8088, user 2964, attachment 2132. Every one of the 2132 attachment carriers has attachment.commandMode == \"task-notification\". queue-operation fields: enqueue with no reason 5803, remove with no reason 2145, remove/absorbed_mid_turn 129, remove/delivered_to_agent 12. Of 2520 main-lane shell launches that were joined to a completion carrier, 1015 (40.3%) have no type:\"user\" carrier at all. Live probe: one Monitor arm produced two notifications, and for both the only on-disk carriers were, in the parent main transcript, ('queue-operation','enqueue',reason None) and ('queue-operation','remove','absorbed_mid_turn') - no user record anywhere.",
"rule": "One carrier observation per (record, <task-notification> section), bucketed by which of the three fields supplied the text. A launch counts as \"no user record\" when every carrier joined to it (by <tool-use-id> first, any <task-id> second) is a queue-operation or attachment.",
"note": "The three-carrier law reproduces exactly, live and in the corpus, and the attachment's commandMode is 100% consistent. The new detail is the `reason` field on the remove line: it names WHY no user record was written, and `delivered_to_agent` is the mechanism behind the BG-014 exception. Both code snippets are verbatim."
}
]
},
{
"id": "BG-014",
"area": "background-tasks",
"behavior": "Background-task launches appear in EVERY lane and completions land in the MAIN transcript: 613 of 624 subagent-lane shell launches were completed there. Notifications are NOT strictly absent from subagent transcripts - when the harness delivers one to an IDLE agent it writes it into that agent's own transcript as an isMeta:true user record (12 measured, in 5 child files, CC 2.1.159 through 2.1.258), and every such delivery is also on the parent's queue-operation enqueue + remove/delivered_to_agent line - so a main-only carrier read still sees all of them.",
"depends": "csift reads launches from every lane but carriers only from the main transcript, resolving the main file from a subagent target; scoping the notification index per file instead of per session root inflated never-returned from 24 to 631, a 26x false-positive rate.",
"code": [
{
"path": "src/live/background.rs",
"lines": "22-27",
"snippet": "//! It rides THREE carriers: a `type:\"user\"` string record when the session was idle,\n//! or (40% of returned shells) a `queue-operation` enqueue + remove and a\n//! `queued_command` attachment when it landed mid-turn - never a user record. A shell\n//! launched from a SUBAGENT lane is completed in the PARENT main transcript (607/618\n//! measured; zero notifications exist in any subagent transcript), so this scan reads\n//! launches from every lane and completions from the main file."
},
{
"path": "src/live/background.rs",
"lines": "250-264",
"snippet": "pub(crate) fn main_transcript_for(path: &Path) -> PathBuf {\n if !crate::subagent::is_subagent_path(path) {\n return path.to_path_buf();\n }\n let mut dir = path.parent();\n while let Some(d) = dir {\n if d.file_name().and_then(|n| n.to_str()) == Some(\"subagents\") {\n if let Some(session_dir) = d.parent() {\n return session_dir.with_extension(\"jsonl\");\n }\n }\n dir = d.parent();\n }\n path.to_path_buf()\n}"
},
{
"path": "src/live/background.rs",
"lines": "303-306",
"snippet": " ingest_launches(&rec, &lane, &mut tasks);\n if is_main {\n ingest_carriers(&rec, &mut carriers, &mut notes);\n }"
}
],
"instrument": "Take a background id launched inside a `subagents/agent-*.jsonl` and `rg -c '<that id>'` in the child file against the parent transcript: the completion appears only in the parent. Counting rule: one launch per (file, tool_use id), `rg -c` counting matching lines; the e2e test `subagent_launches_complete_in_the_parent_main_transcript` in src/live/tests/background_scan.rs pins it.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type",
"observed": "624 subagent-lane shell launches; 613 were closed by a carrier in the PARENT main transcript (98.2%). But 14 <task-notification> sections carrying a <task-id> do sit inside 5 subagent transcripts (record `version` 2.1.252 x11, 2.1.258 x2, 2.1.159 x1). 12 of the 14 are genuine deliveries - isMeta:true with a non-empty <summary> (6 read 'Agent \"...\" finished', 6 read 'Background command \"...\" completed') - and each of those 12 pairs with a main-lane queue-operation enqueue plus a remove whose reason is `delivered_to_agent` for the same task id. The remaining 2 have no <summary> and isMeta null: prose quoting the marker.",
"rule": "One section per (file, <task-notification> block) with at least one <task-id>; lane from the presence of a `subagents` path component; a section counts as a genuine delivery only when the record is isMeta:true and the block has a non-empty <summary>. Pairing = the same task id appearing in a main-lane queue-operation line of the same session root.",
"note": "The operational law csift depends on survives intact: reading carriers from the main file only loses nothing, because the child-lane copy is always shadowed by the parent's queue line. What fails is the absolute wording 'zero notifications exist in any subagent transcript' - it is refuted by 12 genuine records, the oldest at CC 2.1.159, so this is a sampling miss in the original two-specimen measurement, not a version change. All three code snippets are verbatim (src/live/background.rs:22-27, 250-264, 303-306)."
}
]
},
{
"id": "BG-015",
"area": "background-tasks",
"behavior": "A Monitor armed by a SUBAGENT is delivered into that subagent's conversation (the emitter passes the owning agent id), but its <task-notification> is persisted ONLY in the parent MAIN transcript, as a queue-operation enqueue + remove/absorbed_mid_turn pair. Measured at CC 2.1.258 by arming one, and across 39 subagent-lane arms in the corpus: zero monitor notifications in any child transcript.",
"depends": "csift joins every completion through the three carriers `background_scan::carrier_text` reads - a user string record (the idle delivery), a `queue-operation` enqueue line and a `queued_command` attachment (the mid-turn delivery) - and since v0.10.2 `wait --until notification` and the wait activity census read the same three carriers through `delivered_pulse_labels`; a queue `remove`/`dequeue` repeats the enqueue's pulse and delivers nothing. Before v0.10.2 the condition matched only `automation_label`, which requires a `message{}` string, so a pulse absorbed mid-turn (roughly every other completion) could never fire it.",
"code": [
{
"path": "src/live/conditions.rs",
"lines": "16-18",
"snippet": " /// A task-notification lands in the MAIN transcript (they never land in children),\n /// optionally payload-matched.\n Notification(Option<regex::Regex>),"
},
{
"path": "src/live/conditions.rs",
"lines": "111-115",
"snippet": " match cond {\n Cond::Notification(re) => {\n if !is_main {\n return false;\n }"
},
{
"path": "src/model/predicates.rs",
"lines": "180-184",
"snippet": " pub fn automation_label(&self) -> Option<String> {\n let content = self.message.as_ref()?.content.as_ref()?;\n let Content::Text(s) = content else {\n return None;\n };"
},
{
"path": "src/live/background_scan.rs",
"lines": "242-257",
"snippet": "pub(crate) fn delivered_pulse_labels(rec: &Record) -> Vec<String> {\n if rec.is_type(\"queue-operation\") && rec.operation.as_deref() != Some(\"enqueue\") {\n return Vec::new();\n }\n let Some(text) = carrier_text(rec) else {\n return Vec::new();\n };\n text.split(TASK_NOTIFICATION_PREFIX)\n .skip(1)\n .map(|section| {\n crate::model::automation_label_for_section(&format!(\n \"{TASK_NOTIFICATION_PREFIX}{section}\"\n ))\n })\n .collect()\n}"
},
{
"path": "src/live/conditions.rs",
"lines": "112-126",
"snippet": " Cond::Notification(re) => {\n if !is_main {\n return false;\n }\n // The idle delivery is a user record; a pulse absorbed mid-turn lands ONLY\n // on a queue-operation enqueue line and a queued_command attachment\n // (v0.10.2: the three carriers `background_scan` joins, not the user\n // record alone - the agents-stopped notice stays a user-record label).\n let mut labels = crate::live::delivered_pulse_labels(rec);\n if labels.is_empty() {\n let Some(label) = rec.automation_label() else {\n return false;\n };\n labels.push(label);\n }"
}
],
"instrument": "Arm a Monitor from inside a subagent and let it fire, then `rg -c '<task-notification>' <that subagents/agent-*.jsonl>` - a nonzero count refutes the main-only law. Counting rule: one carrier per matching line inside a subagent transcript; for background-shell completions the same count is zero corpus-wide.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "Live probe at CC 2.1.258: Monitor(command='sh -c \\'sleep 3; echo LEDGERPROBE_EVENT_ONE; sleep 120\\'', persistent=false, timeout_ms=20000) armed from inside a running subagent lane, then `rg -c '<task-id>' ~/.claude/projects/<project>/<session>/subagents/workflows/<wf>/agent-<id>.jsonl` against `rg -c '<task-id>' ~/.claude/projects/<project>/<session>.jsonl`, json-decoding every matching line; plus python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Monitor timed out'",
"observed": "The arm returned 'Monitor started (task <9-char id>, timeout 20000ms)'. Both the event pulse (<event>LEDGERPROBE_EVENT_ONE</event>) and the timeout notice (<event>[Monitor timed out \\u2014 re-arm if needed.]</event>, em dash in the real bytes) were DELIVERED into the subagent's own conversation, but on disk both were written ONLY to the parent main transcript, twice each as ('queue-operation','enqueue') and ('queue-operation','remove','absorbed_mid_turn'). The subagent's own transcript holds zero notification records for that task id: its 2 raw byte matches are the probe's own Bash tool_use and its tool_result echoing the id. Corpus agrees: of 39 subagent-lane Monitor arms, 37 notified into the main transcript, 2 never notified, and 0 notified into a child lane. The binary does pass an owning agent id on the emit - JM(f,\"[Monitor timed out \\u2014 re-arm if needed.]\",x,{isHousekeeping:!0,agentId:O}) - but that is a delivery address, not a transcript destination.",
"rule": "One notification per matching line; a line counts as a carrier only when the <task-notification> text comes from a type:\"user\" string content, a queue-operation content, or a queued_command attachment prompt - a tool_use or tool_result that merely contains the id is not a carrier. Landing lane = the presence of a `subagents` component in the path of the file holding the carrier.",
"note": "Refuted by the decisive instrument the claim itself named. The conversational delivery is real - the pulse and the timeout both arrived in the subagent's own turn - which is presumably what the claim generalized from; the on-disk consequence it asserted does not follow. No version change was established: 2.1.258 was measured live and the 39 corpus arms span 2.1.191 to 2.1.252, all behaving the same way. Both code snippets (src/live/conditions.rs:16-18 and 111-115) are verbatim."
},
{
"claude_code": "2.1.258",
"csift": "0.10.2",
"date": "2026-09-03",
"verdict": "drifted",
"instrument": "python line census over the 67 main transcripts of this corpus: every line containing `<task-notification>` bucketed by line type (user record with role user / queue-operation enqueue / other queue operation / attachment)",
"observed": "3218 pulse-bearing user records, 5893 queue enqueue lines, 2315 other queue operations, 4480 attachment carriers: the enqueue count exceeds the user-record count by 2675, the pulses absorbed mid-turn",
"rule": "one line = one carrier; a pulse with an enqueue line and no user record was absorbed mid-turn",
"note": "csift 0.10.2: Cond::Notification and the wait activity census read the enqueue line and the queued_command attachment through live::delivered_pulse_labels (a remove/dequeue counts nothing); pinned by the e2e p12_notification_fires_on_a_queue_enqueue_line_not_on_its_remove and the activity unit test"
}
]
},
{
"id": "BG-016",
"area": "background-tasks",
"behavior": "...an EVENT pulse carrying (task-id, summary, event) and NO <status> (841 sections measured, all with <event>), and a TERMINATION notice carrying (task-id, tool-use-id, output-file, status, summary) whose <status> was completed in 111 of 111 cases.",
"depends": "csift keeps a monitor OPEN through event pulses (a carrier with no `<status>` and no timeout event leaves the state untouched) and closes it only on the termination notice or a timeout event; treating a pulse as a completion closes a persistent monitor on its first event. The rendered label likewise falls back to the `<event>` payload instead of fabricating `completed`.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "309-322",
"snippet": " let state = if c.orphan_summary {\n orphaned += 1;\n Some(BgState::Stopped)\n } else if c.status.is_some() {\n Some(BgState::from_status(c.status.as_deref()))\n } else if c\n .event\n .as_deref()\n .is_some_and(|e| e.to_ascii_lowercase().contains(\"timed out\"))\n {\n Some(BgState::TimedOut)\n } else {\n None // a Monitor event pulse: the monitor is still armed\n };"
},
{
"path": "src/model/automation.rs",
"lines": "93-97",
"snippet": " /// The `<event>` payload, if present - where a Monitor / ScheduleWakeup pulse carries its\n /// real outcome (`STAGE2_OUTPUT_READY`, `[Monitor timed out - re-arm if needed.]`). Often\n /// the only outcome signal on a Monitor pulse (which usually has no `<status>`), so the\n /// label falls back to it instead of fabricating `completed`.\n pub event: Option<String>,"
}
],
"instrument": "On a monitor-rich transcript, extract every `<task-notification>` block in `type:\"user\"` records whose `<summary>` first word is `Monitor` and bucket by presence of `<status>` against `<event>`. Counting rule: one observation per notification BLOCK (a record may hold several), counting only delivered `type:\"user\"` records so the queue and attachment echoes are not triple-counted; the unit test `a_monitor_is_open_through_event_pulses_until_it_ends_or_times_out` in src/live/tests/background_scan.rs pins the state machine.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "src/model/automation.rs comment; src/live/background.rs module doc; CHANGELOG 0.10.0; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 bgverify6.py / bgverify7.py - the same walk restricted to type:\"user\" string records, censusing <status>, <summary> shape and <task-id> length per notification section; plus the live Monitor probe described under BG-015",
"observed": "Delivered type:\"user\" notification sections whose <summary> starts with 'Monitor': 841 event pulses carrying <event> and NO <status>, plus 111 termination notices carrying <status>, and <status> was completed in 111 of 111. Zero sections had neither a status nor an event. The live probe reproduced both shapes from one arm: an event pulse (<summary>Monitor event: \"...\"</summary><event>LEDGERPROBE_EVENT_ONE</event>, no status) and then the timeout pulse, also with no status.",
"rule": "One observation per <task-notification> section in a delivered type:\"user\" record whose <summary> begins with the word Monitor, so the queue and attachment echoes are not triple-counted; bucketed by presence of <status> against presence of <event>.",
"note": "The 111-of-111 figure reproduces exactly; the pulse count grew 828 -> 841 with the corpus. The two shapes are cleanly separated - no section carries both, none carries neither - so csift's rule (a carrier with no status and no timeout event leaves the state untouched) has no ambiguous input in this corpus. Both code snippets are verbatim (src/live/background_scan.rs:279-292, src/model/automation.rs:93-97)."
}
]
},
{
"id": "BG-017",
"area": "background-tasks",
"behavior": "A Monitor timeout is delivered as an event pulse whose <event> payload is the literal [Monitor timed out \\u2014 re-arm if needed.] - with an EM DASH (U+2014), not a hyphen. It is emitted from exactly two sites in the binary and never from a Monitor tool_result; 51 of the 54 corpus sections carrying the phrase have it inside <event>, the other 3 being one async-agent completion notification that quotes it. It fires only for a non-persistent monitor - the timeout timer is not scheduled when persistent is set.",
"depends": "csift closes a monitor as `timed-out` on a case-insensitive `timed out` substring of the `<event>` payload rather than on the exact bracketed literal, so a reworded template still closes the task.",
"code": [
{
"path": "src/live/background.rs",
"lines": "44-74",
"snippet": "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum BgState {\n /// Launched, no completion carrier names it yet.\n Open,\n Completed,\n Failed,\n Killed,\n /// Claude Code's own orphan reconciliation at the next session start, or an\n /// explicit `stopped` status.\n Stopped,\n /// A Monitor whose timeout fired (the `[Monitor timed out …]` event).\n TimedOut,\n}"
},
{
"path": "src/live/background_scan.rs",
"lines": "309-322",
"snippet": " let state = if c.orphan_summary {\n orphaned += 1;\n Some(BgState::Stopped)\n } else if c.status.is_some() {\n Some(BgState::from_status(c.status.as_deref()))\n } else if c\n .event\n .as_deref()\n .is_some_and(|e| e.to_ascii_lowercase().contains(\"timed out\"))\n {\n Some(BgState::TimedOut)\n } else {\n None // a Monitor event pulse: the monitor is still armed\n };"
}
],
"instrument": "`rg -o 'Monitor timed out.{0,30}' ~/.claude/projects --glob '*.jsonl'` and classify each occurrence by its enclosing structure (inside `<event>` against quoted in some other tool's result text). Counting rule: one occurrence per structural position; the raw line count over-reads about elevenfold because csift's own source and docs quote the literal.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Monitor timed out'; python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type; plus the live Monitor probe described under BG-015",
"observed": "The real bytes use an EM DASH: the live probe delivered <event>[Monitor timed out \\u2014 re-arm if needed.]</event>, and the binary has exactly two emit sites, both JM(<sink>,\"[Monitor timed out \\u2014 re-arm if needed.]\",<taskId>,{isHousekeeping:!0,agentId:<id>}) - one on the shell-monitor path, one on the websocket path. Corpus: 54 notification sections contain the phrase, 51 inside an <event> tag and 3 outside it; the 3 are one async-agent completion notification (the same task id, its main-lane queue pair plus one child-lane copy) whose body quotes the phrase - none is a Monitor tool_result. Non-persistence is structural in the binary: `let D=C?void 0:setTimeout((E,f,x,O,I)=>{...},T,W,g,v.taskId,y,p)` where C is the persistent flag, so no timer is scheduled at all for a persistent monitor; 274 of 432 corpus arms are persistent.",
"rule": "One occurrence per <task-notification> section containing the phrase, classified by whether the section's <event> value itself contains it. The claim's raw `rg -o` over the corpus over-reads by roughly elevenfold because csift's own source, docs and dev transcripts quote the literal.",
"note": "csift closes a monitor as timed-out on a case-insensitive `timed out` substring of the <event> payload. That is looser than it looks in this corpus: 6 Monitor event pulses carry payloads ending 'ssh=Connection to 127.0.0.1 port 2224 timed out', i.e. the MONITORED command's own output, which the substring rule would book as a monitor timeout. Matching the bracketed literal (or requiring the phrase at the payload start) would remove that false positive. Both code snippets are verbatim."
}
]
},
{
"id": "BG-018",
"area": "background-tasks",
"behavior": "Almost every armed monitor does notify: only 7 of 430 distinct armed task ids (1.6%) produced no notification at all. The 30% figure is a counting-rule artifact - counting only notifications delivered as a type:\"user\" record gives 129 of 430 (30.0%), because the other monitors' pulses were all absorbed mid-turn onto the queue-operation and attachment carriers (the BG-013 path).",
"depends": "csift reports an armed monitor as `open` with the explicit honesty note rather than inferring it is still live; absence of an event is never evidence of absence, and a persistent monitor must never be reported as failed.",
"code": [
{
"path": "src/live/background.rs",
"lines": "10-17",
"snippet": "//! - a MONITOR: the `Monitor` tool_use (a command whose stdout lines are events, or a\n//! websocket), armed on disk as an immediately-paired pair whose result reads `Monitor\n//! started (task <id>, …)` with `toolUseResult.taskId`. It shares the `b…` id namespace\n//! with backgrounded shells (both are `local_bash` tasks in the harness), so only the\n//! tool name tells them apart. Event pulses (`Monitor event: …`, no `<status>`) never\n//! close it; a termination notice (`<status>completed</status>`, summary opening\n//! `Monitor`) or a timeout event does; a PERSISTENT monitor never returns by design\n//! (measured: 30% of armed monitors produced no notification at all)."
}
],
"instrument": "Build the set of armed monitor task ids (from `Monitor started (task <id>` result texts) and the set of fired ids (from `<task-id>` inside Monitor-summary notifications) and take the difference. Counting rule: distinct ids on both sides, deduped on the id string because one session copied into two project directories duplicates ids.",
"located": {
"claude_code": "2.1.252",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6.13; src/live/background.rs module doc; CHANGELOG 0.10.0; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type",
"observed": "432 Monitor arms resolving to 430 distinct task ids (every id recovered from the arm's own result text 'Monitor started (task <id>'). Counting fired ids across ALL THREE carriers, only 7 of 430 armed monitors (1.6%) produced no notification at all - 3 non-persistent and 4 persistent. Restricting the fired set to notifications delivered as a type:\"user\" record raises that to 129 of 430 (30.0%), which reproduces the ledger's 132 of 426. By lane: 388 of 393 main-lane arms and 37 of 39 subagent-lane arms notified.",
"rule": "Distinct task-id strings on both sides. Armed = the id in the arm's tool_result text. Fired = the id appears in any <task-notification> <task-id> tag within the same session root; the two variants differ only in whether queue-operation and attachment carriers count as a firing.",
"note": "This is the sharpest correction in the batch: the ledger's headline number measured 'never delivered as a user record', not 'never fired'. The honesty conclusion csift draws is unchanged and still right - an armed monitor is reported open, never inferred live - but the src/live/background.rs module doc's parenthetical '(measured: 30% of armed monitors produced no notification at all)' should read 1.6%, or say 'no user-record notification'. The code snippet at src/live/background.rs:10-17 is verbatim, including that parenthetical."
}
]
},
{
"id": "BG-019",
"area": "background-tasks",
"behavior": "...and 12 of 476 joined launches notified two or four times (11 twice, 1 four times); 169 of the 476 were closed with no user-record delivery at all - their notifications rode the queue/attachment carriers.",
"depends": "csift's carrier resolution takes the LATEST carrier per task, so a resumed agent's re-notification overwrites the earlier state rather than double-closing it.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "279-281",
"snippet": "/// Join carriers to launches: `<tool-use-id>` first (exact), any `<task-id>` second.\n/// The latest carrier wins (an agent notifies again after a resume).\npub(crate) fn resolve_carriers("
}
],
"instrument": "`rg -c '<note>' ~/.claude/projects --glob '*.jsonl'` and, for one agent id, count the distinct line numbers of `<task-notification>` records naming it. Counting rule: one notification per distinct matching line in the main transcript; more than one line for one id is a re-notification.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 bgverify2.py - a walk of ~/.claude/projects/**/*.jsonl that json-parses only lines carrying one of the byte needles run_in_background | 'Command running in background' | async_launched | task-notification | 'Monitor started' | '\"Monitor\"' | queued_command | backgroundTaskId, ingests launches exactly as src/live/background_scan.rs ingest_launches does, and reads notifications ONLY from the three carrier fields ingest_carriers reads; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'A task-notification fires each time this agent'",
"observed": "The <note> text is verbatim in the binary, closing tag included: 'A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>', and it appears on 135 delivered user-record sections in the corpus. Of 482 async-agent launches, 476 joined to at least one carrier; counting distinct user-record delivery instants per launch gives 0 -> 169 launches, 1 -> 295, 2 -> 11, 4 -> 1. So 12 launches notified two or four times.",
"rule": "One launch per (file, tool_use id) whose toolUseResult.status is async_launched; one delivery per distinct timestamp among that launch's type:\"user\" carriers, so the queue-operation and attachment echoes of one delivery are not counted as repeats.",
"note": "The count of repeat-notifiers reproduces exactly at 12; only the denominator changes (158 -> 476 joined launches). The non-terminality is confirmed at the source: the harness ships the caveat in the notification itself, so csift's latest-carrier-wins resolution is reading a documented contract, not guessing. The code snippet at src/live/background_scan.rs:249-251 is verbatim."
}
]
},
{
"id": "BG-020",
"area": "background-tasks",
"behavior": "...plus a sentinel <task-id>__orphan_summary__:<kind></task-id> where kind is one of agent | shell | workflow (only :shell observed here), <status>stopped</status>, and a summary reading 'N background <kind> task(s) from the previous session have no completion record ... They may have been stopped (via the UI, Monitor timeout, or agent teardown \\u2014 these leave no transcript marker)' - an em dash, not a hyphen. The harness also uses a SECOND sentinel form, __orphan_summary_live__:<id>, to exclude a still-live task from the summary.",
"depends": "csift reads ALL `<task-id>` tags (not just the first), marks every named task `stopped` and discloses the count; that harness sentence is the honesty bound csift adopts - NOT RETURNED IS NOT PROOF OF STILL RUNNING - so absence of a completion is never reported as proof of liveness.",
"code": [
{
"path": "src/live/background.rs",
"lines": "29-35",
"snippet": "//! At the next session start Claude Code reconciles orphans itself: one notification\n//! carrying several `<task-id>` tags plus `__orphan_summary__:shell`, status `stopped`,\n//! whose summary says the tasks \"may have been stopped (via the UI, Monitor timeout, or\n//! agent teardown - these leave no transcript marker)\". That sentence is the honesty\n//! bound: NOT RETURNED IS NOT PROOF OF STILL RUNNING. The `<output-file>` from the\n//! launch is a real file (an agent's is a symlink to its transcript); its size and\n//! mtime are an independent \"still producing output\" signal, one `stat` per open task."
},
{
"path": "src/live/background_scan.rs",
"lines": "205-216",
"snippet": " for section in text.split(TASK_NOTIFICATION_PREFIX).skip(1) {\n let task_ids = all_xml_tags(section, \"task-id\");\n let orphan = task_ids.iter().any(|t| t.starts_with(\"__orphan_summary__\"));\n carriers.push(Carrier {\n task_ids,\n tool_use_id: extract_xml_tag(section, \"tool-use-id\"),\n status: extract_xml_tag(section, \"status\"),\n event: extract_xml_tag(section, \"event\"),\n ts: rec.timestamp.clone(),\n orphan_summary: orphan,\n });\n }"
},
{
"path": "src/live/background_scan.rs",
"lines": "260-277",
"snippet": "pub(crate) fn all_xml_tags(s: &str, tag: &str) -> Vec<String> {\n let open = format!(\"<{tag}>\");\n let close = format!(\"</{tag}>\");\n let mut out = Vec::new();\n let mut at = 0usize;\n while let Some(i) = s[at..].find(&open) {\n let start = at + i + open.len();\n let Some(j) = s[start..].find(&close) else {\n break;\n };\n let inner = s[start..start + j].trim();\n if !inner.is_empty() {\n out.push(inner.to_string());\n }\n at = start + j + close.len();\n }\n out\n}"
},
{
"path": "src/live/background_scan.rs",
"lines": "330-336",
"snippet": " if orphaned > 0 {\n notes.push(format!(\n \"{orphaned} task(s) were reconciled as stopped by Claude Code at a later session \\\n start (its orphan summary: no completion record; a UI stop, a Monitor timeout or \\\n agent teardown leaves no transcript marker)\"\n ));\n }"
}
],
"instrument": "`rg -l '__orphan_summary__:' ~/.claude/projects --glob '*.jsonl'`, then parse a matching line and count its `<task-id>` tags; `csift status @<that session> --format json | jq '.background.notes'` shows the reconciliation note. Counting rule: one reconciliation record per matching line; a record with N real ids plus one `__orphan_summary__:` sentinel reconciles N tasks.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; SPEC.md section 6.13; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'orphan_summary|no completion record'",
"observed": "5 sections in the corpus carry an __orphan_summary__ sentinel. They are 2 genuine reconciliation events, each written twice - once as a queue-operation line and once as a type:\"user\" record about 90 ms later - naming 3 and 2 real task ids respectively, plus the sentinel __orphan_summary__:shell and <status>stopped</status>; the fifth section is prose quoting the marker. The summary text matches the claim except for an EM DASH: 'They may have been stopped (via the UI, Monitor timeout, or agent teardown \\u2014 these leave no transcript marker)'. The binary carries the template - '${r.length} background ${o} task(s) from the previous session have no completion record. ... They have been marked ${e}. ... Task ids in this notification beginning with \"${Oe}\" are internal scan markers, not tasks.' - and the sentinel family: Oe=\"__orphan_summary\",tt=`${Oe}__:`,nt=`${Oe}_live__:` with if(d===\"agent\"||d===\"shell\"||d===\"workflow\")o.summarizedKinds.add(d).",
"rule": "One reconciliation record per matching line; a record with N real ids plus one __orphan_summary__: sentinel reconciles N tasks; a line whose section has no <task-id> is prose quoting the marker and is dropped. Distinct events counted by (timestamp, summary).",
"note": "The mechanism and the honesty sentence both hold, verified at the binary and in the corpus. Two gaps worth carrying: csift's `t.starts_with(\"__orphan_summary__\")` does not match the __orphan_summary_live__: form (0 occurrences in this corpus, so untested in practice), and the sentinel kind can be agent or workflow, not only shell - the note csift renders says 'shell' nowhere, so it is safe either way. All three code snippets are verbatim (src/live/background_scan.rs:215-226, 230-247, 300-306)."
}
]
},
{
"id": "BG-021",
"area": "background-tasks",
"behavior": "Orphan reconciliation aggregates by KIND with different thresholds - shells aggregate above 1 orphan (`i.length>1`), agents and workflows only above 20 (`length>Fe`, Fe=20). The aggregate marks agents `failed` and shells/workflows `stopped`. A truncated aggregate says `First 20 task ids:` and an untruncated one says `Task ids:`. The two sentinels are `__orphan_summary__:` (kind suffix) and `__orphan_summary_live__:` (one per live exclusion); NEITHER has a genuine on-disk specimen in this corpus - all 90 matched records for the two literals are dev-session prose. A single orphan below the threshold arrives as an ordinary one-id notification with <status>stopped</status> (3 on-disk specimens, all shells).",
"depends": "csift's orphan detection keys on the `__orphan_summary__` prefix of a `<task-id>`, so a below-threshold singular orphan is handled by the normal carrier path with no special case.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "205-216",
"snippet": " for section in text.split(TASK_NOTIFICATION_PREFIX).skip(1) {\n let task_ids = all_xml_tags(section, \"task-id\");\n let orphan = task_ids.iter().any(|t| t.starts_with(\"__orphan_summary__\"));\n carriers.push(Carrier {\n task_ids,\n tool_use_id: extract_xml_tag(section, \"tool-use-id\"),\n status: extract_xml_tag(section, \"status\"),\n event: extract_xml_tag(section, \"event\"),\n ts: rec.timestamp.clone(),\n orphan_summary: orphan,\n });\n }"
},
{
"path": "src/live/background_scan.rs",
"lines": "309-322",
"snippet": " let state = if c.orphan_summary {\n orphaned += 1;\n Some(BgState::Stopped)\n } else if c.status.is_some() {\n Some(BgState::from_status(c.status.as_deref()))\n } else if c\n .event\n .as_deref()\n .is_some_and(|e| e.to_ascii_lowercase().contains(\"timed out\"))\n {\n Some(BgState::TimedOut)\n } else {\n None // a Monitor event pulse: the monitor is still armed\n };"
}
],
"instrument": "Read the aggregation templates out of the installed Claude Code binary's own strings (`strings <binary> | grep __orphan_summary`), and corpus-side count the `<task-id>` tags on each orphan notification. Counting rule: one template per distinct literal; the aggregation branch is a single threshold comparison per kind.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -a -n 6 ~/.local/share/claude/versions/2.1.258 > cc258a.strings; rg -o '.{300}__orphan_summary.{800}' cc258a.strings; rg -a -o -b 'Oe=\"__orphan_summary\"' ~/.local/share/claude/versions/2.1.258 (byte offset 178948531) then dd bs=1 skip=178943000 count=60000 | tr -d '\\0' > orphan_region.txt; rg -o 'function st\\(.{0,1400}' orphan_region.txt; csift search '__orphan_summary__:' --count-by label; csift search '__orphan_summary_live__:' --count-by label; csift search 'No completion record was found' -t harness.notification --raw",
"observed": "binary: `Fe=20,In=172800000,Oe=\"__orphan_summary\",tt=`${Oe}__:`,nt=`${Oe}_live__:``. Shell branch: `resume: ${i.length} background shell command(s) orphaned by previous process exit`),i.length>1){...st(\"stopped\",\"shell\",\"shell command\",i,...)`. Agent branch: `c.length>Fe){...st(\"failed\",\"agent\",\"agent\",c,...)`. Workflow branch: `i.length>Fe){...st(\"stopped\",\"workflow\",\"workflow\",i,...)`. Aggregate template: `I=c.length===r.length?`Task ids: ${c.join(\", \")}.`:`First ${Fe} task ids: ${c.join(\", \")}.`` plus the trailing sentence `Task ids in this notification beginning with \"${Oe}\" are internal scan markers, not tasks.`. Corpus: 3 records classified harness.notification.task carry `No completion record was found for this background shell command from the previous session`, each with exactly 1 <task-id> tag and <status>stopped</status>. `__orphan_summary__:` matched 70 records across 8 label keys, 0 of them under any harness.notification.* leaf; `__orphan_summary_live__:` matched 20 records across 6 label keys, 0 under any harness.notification.* leaf.",
"rule": "Binary side: one template per distinct string literal; one threshold comparison per orphan kind (three comparison sites total). Corpus side: one observation per record csift 0.10.0 emits under `-t harness.notification` over all 7611 sessions in scope; a sentinel counts as having an on-disk specimen only if a matched record carries a harness.notification.* label (a match under agent.* / user.* is prose quoting the sentinel, not a harness-written notice).",
"note": "Both code snippets are verbatim in the current tree at the stated lines (src/live/background_scan.rs:215-226 and 279-292). The only correction is the sentinel-specimen claim: the ledger implies `__orphan_summary__:` has an on-disk specimen and only the `_live__` twin lacks one; measured, neither does, so csift's aggregate-orphan path (`orphan_summary: true` -> BgState::Stopped) has never been exercised by real corpus data on this machine, only by fixtures."
}
]
},
{
"id": "BG-022",
"area": "background-tasks",
"behavior": "Agent orphans branch three ways - handed to auto-resume, `stopped`, or `failed`. Auto-resume requires FOUR conditions together, not two: an auto-resume callback is present, the agent was launched by the Agent tool or by a forked skill, its meta is fetchable, and its transcript mtime is younger than 172800000 ms (48 hours). Otherwise the split is `stopped` when the transcript mtime is known OR the transcript is fetchable (or the agent was redispatched), and `failed` when neither holds - the harness's own wording for `failed` is that its in-process state was lost. That 48-hour constant is the only aging rule in the whole orphan family (one comparison site; the module carries no other 4+-digit literal).",
"depends": "csift does not implement resume prediction, but the rule bounds what an OPEN async agent can mean: a lane younger than 48 hours may be auto-resumed rather than orphaned, so `open` is genuinely undecidable there.",
"code": [
{
"path": "src/live/background.rs",
"lines": "44-74",
"snippet": "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum BgState {\n /// Launched, no completion carrier names it yet.\n Open,\n Completed,\n Failed,\n Killed,\n /// Claude Code's own orphan reconciliation at the next session start, or an\n /// explicit `stopped` status.\n Stopped,\n /// A Monitor whose timeout fired (the `[Monitor timed out …]` event).\n TimedOut,\n}"
}
],
"instrument": "Read the constant from the installed Claude Code binary's orphan-reconciliation strings (`strings <binary> | grep 172800000`), and corpus-side look for a second launch in the same agent lane after a gap. Counting rule: one constant, one comparison site.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -a -o -b 'Oe=\"__orphan_summary\"' ~/.local/share/claude/versions/2.1.258; dd if=<binary> bs=1 skip=178943000 count=60000 | tr -d '\\0' > orphan_region.txt; python slice of orphan_region.txt around 'background agent(s) orphaned'; python regex census of 4+-digit literals over the module span (from `var An=new RegExp` to `function $n({calls:e`)",
"observed": "`Fe=20,In=172800000`. Auto-resume gate: `if(r!==void 0&&(A.launchedByAgentTool===!0||A.launchedByForkedSkill===!0)&&v&&p!==null&&_-p<In){I.push({agentId:A.agentId,description:A.description,outputFile:A.outputFile,isWebFetchLaunch:A.isWebFetchLaunch});continue}` followed by `let U=p!==null||O,q=A.redispatched||U?\"stopped\":\"failed\"`. Handoff log: `resume: handing ${I.length} disk-resumable orphaned agent(s) to the bg auto-resume path`. The stopped summary reads `either way its transcript is saved, so its progress is not lost`; the failed summary reads `Its in-process state was lost.` Module span = 13725 bytes and contains exactly one numeric literal of 4 or more digits: 172800000 (count of the string in the whole 60000-byte region: 2, both the declaration and no second comparison).",
"rule": "One constant, one comparison site. An 'aging rule' counts as a comparison of a now-timestamp against a stored millisecond timestamp; the census scanned every 4+-digit numeric literal in the orphan module and found only 172800000 (Fe=20 is a count threshold, not an age).",
"note": "The ledger's paraphrase 'stopped when the transcript survives' is close but the actual predicate is mtime-known OR fetchable, and a redispatched agent short-circuits to `stopped` regardless. The csift snippet (src/live/background.rs:62-74, the BgState enum) is verbatim in the current tree."
}
]
},
{
"id": "BG-023",
"area": "background-tasks",
"behavior": "Every orphan notice is enqueued with `shouldQuery:false`, whose own schema prose reads `appended to the transcript without triggering an assistant turn. It will be merged into the next user message that does query`; the notice nonetheless lands as a `type:\"user\"` record whose parentUuid chain is followed by a real assistant turn 52-86 seconds later (measured 51.8 / 60.8 / 85.8 s on the 3 corpus specimens).",
"depends": "csift keeps the orphan notice as a turn opener (`harness.notification.*`) rather than dropping it, because a genuine generation follows it through the attachment chain; classifying it as non-delivered would lose a real turn boundary.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "325-329",
"snippet": " // The harness's agents-stopped notice (v0.10.0): a kill notice, not the human.\n if is_agents_stopped_notice(s) {\n push_unique(out, Class::NotificationSubagent);\n return;\n }"
}
],
"instrument": "Locate an orphan notice line and walk forward: the next record is an `attachment` at delta about 0 whose `parentUuid` equals the notice `uuid`, and the first assistant record follows tens of seconds later. Counting rule: one walk per specimen; timestamp deltas in seconds between the notice and the first following `type:\"assistant\"` record.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -a -o 'shouldQuery:M\\(\\).optional\\(\\).describe\\(\"[^\"]{0,300}' ~/.local/share/claude/versions/2.1.258 ; rg -a -o 'appended to the transcript[^\"`]{0,200}' ~/.local/share/claude/versions/2.1.258 ; python walk over the 3 corpus specimens found by `csift search 'No completion record was found for this background shell' -t harness.notification --format json` (session id + line), reading the owning .jsonl directly",
"observed": "Schema prose: `shouldQuery:M().optional().describe(\"When false, the message is appended to the transcript without triggering an assistant turn. It will be merged into the next user message that does query.` Both orphan emit sites pass `shouldQuery:!1` (the singular `Ra({value:ka({...status:\"stopped\"...}),agentId:Ze(),mode:\"task-notification\",skipAttachments:!0,priority:\"next\",shouldQuery:!1})` and the aggregate `st(...)`). Corpus, 3/3 specimens: the record is `type:\"user\"`; the very next line is `type:\"attachment\"` at delta 0.0 s whose parentUuid equals the notice uuid; the first following `type:\"assistant\"` record lands at +51.849 s, +85.804 s and +60.753 s; walking that assistant record's parentUuid chain reaches the notice in 3/3.",
"rule": "One walk per specimen. Delta = seconds between the notice's `timestamp` and the `timestamp` of the first following record with `type:\"assistant\"` in the same file. Chain check = follow `parentUuid` upward through a uuid index of the following 40 lines until the notice uuid is reached or the chain runs out (cap 12 hops).",
"note": "Only the seconds range needed correction; the structural half (next line an attachment at delta 0 parented to the notice, then a real assistant turn whose parentUuid chain reaches the notice) reproduced 3/3. In 2 of the 3 specimens a genuine user record intervenes between the notice and the assistant turn, so the delta measures 'time until the next generation', not 'time the notice took to trigger one' - the notice by construction triggers none. The csift snippet (src/model/classify.rs:325-329) is verbatim in the current tree."
}
]
},
{
"id": "BG-024",
"area": "background-tasks",
"behavior": "The stopped/orphan notice family shares MOST of one record shape: `type:\"user\"`, `userType:\"external\"`, `origin:{\"kind\":\"task-notification\"}` and a `message.content` that is a STRING (never an array) hold 2700/2700. The rest are near-universal, not universal: `promptSource:\"system\"` on 2576/2700 (absent on 124), `isSidechain:false` on 2698/2700, and the `isMeta` key IS present (true) on 2/2700 - so 'no isMeta key at all' is false. The measured XML/plain split was 2689 XML-wrapped against 11 plain-text out of 2700, not 2683/17.",
"depends": "csift classifies these by CONTENT shape (the `<task-notification>` prefix, or the agents-stopped templates) rather than by `origin` / `promptSource`, so a plain-text notice with no recognizable template still classifies as the human; those two fields are the authoritative discriminator csift does not yet read.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "325-329",
"snippet": " // The harness's agents-stopped notice (v0.10.0): a kill notice, not the human.\n if is_agents_stopped_notice(s) {\n push_unique(out, Class::NotificationSubagent);\n return;\n }"
}
],
"instrument": "`rg -l '\"origin\":\\{\"kind\":\"task-notification\"\\}' ~/.claude/projects --glob '*.jsonl'`, then for each matching record test whether the content starts with `<task-notification>`. Counting rule: one observation per RECORD whose parsed `origin.kind == \"task-notification\"`.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' -t harness.notification --raw --max-count 0 > notif_raw.jsonl (2700 lines, whole corpus) + python field census; cross-check scoped to ONE project directory: rg -a -c --no-filename '\"origin\":\\{\"kind\":\"task-notification\"' ~/.claude/projects/<one-project-dir> --glob '*.jsonl' | paste -sd+ | bc versus csift search '' -t harness.notification --raw --max-count 0 @<that-project-dir>",
"observed": "n=2700. origin == {\"kind\":\"task-notification\"}: 2700/2700. message.content is a STRING: 2700/2700 (0 arrays). type:\"user\" + userType:\"external\": 2700/2700. isSidechain:false 2698, isSidechain:true 2. promptSource:\"system\" 2576, key absent 124. `isMeta` key PRESENT on 2 records (both true, both isSidechain:true). Content shape: 2689 begin with `<task-notification>`, 11 do not (9 agents-stopped notices, 2 wrapped in a `[SYSTEM NOTIFICATION - NOT USER INPUT]` hook preamble that still contains a <task-notification> block). Scoped cross-check: rg counts 184 origin-keyed lines, csift emits 183 - the one difference is an isMeta:true / isSidechain:true type:\"user\" record in a subagent lane whose content is the hook preamble followed by a <task-notification> block; csift classifies it to an EMPTY label set (`csift search '' @<that-lane> --count-by label` returns 453 records across 6 labels, none harness or user, and `csift show @<that-lane> --line <N>` answers `no such record(s)`).",
"rule": "Primary census: one observation per RECORD csift 0.10.0 emits under `-t harness.notification --raw` over all 7611 sessions in scope; each field counted once per record. Cross-check census: one observation per raw line in one project directory containing the compact byte pair `\"origin\":{\"kind\":\"task-notification\"`.",
"note": "The ledger's 'depends' clause is materially understated and is now a measured csift gap: keying on content shape rather than on `origin`/`promptSource` does not merely mislabel an unrecognized notice as the human - at least one origin-keyed notice (isMeta:true, in a subagent lane, hook preamble before the <task-notification> block) classifies to an EMPTY label set and becomes invisible to `search` under every selector AND unaddressable by `show --line`. Two structurally identical records elsewhere in the corpus DO surface under -t harness.notification, so the discriminator was not isolated here; what is decided is that the two censuses disagree by exactly one record in the one directory measured. Note also that the primary census is csift-keyed, so the true origin-keyed population is >= 2700."
}
]
},
{
"id": "BG-025",
"area": "background-tasks",
"behavior": "When background agents are stopped from the UI, Claude Code writes a PLAIN-TEXT `type:\"user\"` record with no XML at all, in two templates: `Background agent \"<desc>\" was stopped by the user.` for one agent and `N background agents were stopped by the user: \"a\", \"b\".` for several - naming a count and truncated prompt prefixes but never an agent id, so the record carries no join key to the tasks it names.",
"depends": "csift classifies both forms `harness.notification.subagent` through a synthetic-marker predicate (the singular form has no leading digit, so a digit-led predicate alone leaks it back to `user.message` as a human turn), never lets it open a turn, renders `[subagent stopped] ...`, and - because the notice names no id - emits an un-joinable note rather than marking any specific task stopped.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "54-75",
"snippet": "/// The harness's \"N background agent(s) were stopped by the user: ...\" notice (v0.10.0):\n/// a plain-string `type:\"user\"` record Claude Code writes when async agents are killed\n/// from the UI. It names a count and truncated prompt prefixes, never an id, and it\n/// triggers no generation - the model sees it only alongside the next real prompt. Not\n/// the operator, never a turn opener; classifies `harness.notification.subagent`.\n#[must_use]\npub fn is_agents_stopped_notice(content: &str) -> bool {\n let s = content.trim_start();\n // The singular template names the agent: `Background agent \"<desc>\" was stopped by\n // the user.` (no count).\n if s.starts_with(\"Background agent \\\"\") && s.contains(\" was stopped by the user\") {\n return true;\n }\n let digits = s.bytes().take_while(u8::is_ascii_digit).count();\n if digits == 0 {\n return false;\n }\n let rest = &s[digits..];\n (rest.starts_with(\" background agent was stopped by the user\")\n || rest.starts_with(\" background agents were stopped by the user\"))\n && rest.contains(AGENTS_STOPPED_MARKER)\n}"
},
{
"path": "src/model/classify.rs",
"lines": "325-329",
"snippet": " // The harness's agents-stopped notice (v0.10.0): a kill notice, not the human.\n if is_agents_stopped_notice(s) {\n push_unique(out, Class::NotificationSubagent);\n return;\n }"
},
{
"path": "src/live/background_scan.rs",
"lines": "188-204",
"snippet": " if crate::model::is_agents_stopped_notice(&text) {\n // The notice rides both a queue enqueue line and the user record: one note per\n // (count, second). It names no id, so csift cannot say WHICH agents it stopped.\n let head = text.trim_start();\n let n = head.bytes().take_while(u8::is_ascii_digit).count();\n let count = if n == 0 { \"1\" } else { &head[..n] };\n let ts = rec.timestamp.as_deref().unwrap_or(\"?\");\n let note = format!(\n \"{count} background agent(s) were stopped by the user at {} - the notice names \\\n no id, so csift cannot mark which agents it stopped\",\n ts.get(..19).unwrap_or(ts)\n );\n if !notes.contains(¬e) {\n notes.push(note);\n }\n return;\n }"
}
],
"instrument": "`rg -c 'background agents were stopped by the user:' ~/.claude/projects --glob '*.jsonl'` and `rg -c 'Background agent \"' ...`; then `csift show @<session> --line <N> --format json | jq .label` on a hit expects `harness.notification.subagent`. Counting rule: one notice per matching line, both templates counted; the e2e test `the_agents_stopped_notice_is_harness_not_human` in tests/cli/live/background.rs pins the classification.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md v0.10.0 ledger; CHANGELOG 0.10.0; src/model/markers.rs comment; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -a -o '.{260}mode:\"task-notification\".{80}' ~/.local/share/claude/versions/2.1.258 (the gesture-handler site); python template census over notif_raw.jsonl (the whole-corpus `csift search '' -t harness.notification --raw` dump); csift search 'stopped by the user' --count-by label; csift show @<session> --line <N> --format json for one plural and one singular specimen",
"observed": "Binary, verbatim: `let Fo=kn.length===1?`Background agent \"${kn[0]}\" was stopped by the user.`:`${kn.length} background agents were stopped by the user: ${kn.map((jn)=>`\"${jn}\"`).join(\", \")}.`;return Ye.enqueuePendingNotification({agentId:Ze(),value:Fo,mode:\"task-notification\",skipAttachments:!0})`. Corpus: 9 plain-text specimens (8 plural, 1 singular); 9/9 contain no `a<16hex>` agent-id token and no `toolu_` token; 9/9 truncate the named prompts with an ellipsis inside the quotes. `--count-by label` over 'stopped by the user': 10 records under harness.notification.subagent. `csift show --line` on a plural specimen: label harness.notification.subagent, labels ['harness.notification.subagent']; on the singular specimen: the same.",
"rule": "One notice per matching record, both templates counted, over all 7611 sessions in scope. 'Names no id' = no substring matching the agent-id shape `a[0-9a-f]{16,17}` and no `toolu_` prefix anywhere in the content.",
"note": "Both templates are byte-verbatim in the 2.1.258 binary and both classify harness.notification.subagent on real records. The two named e2e tests exist: tests/cli/live/background.rs:357 the_agents_stopped_notice_is_harness_not_human and tests/cli/live/background.rs:404 rows_name_a_subagent_lane_and_a_live_output_file. All three csift snippets (src/model/markers.rs:49-70, src/model/classify.rs:325-329, src/live/background_scan.rs:198-214) are verbatim in the current tree. The `--count-by label` figure is 10 rather than 9 because one XML `<task-notification>` whose summary contains the phrase also carries that leaf."
}
]
},
{
"id": "BG-026",
"area": "background-tasks",
"behavior": "The interrupt gesture kills only tasks its predicate admits: a `local_agent` that is running (or completed with work still pending) and an `in_process_teammate` that is running. Backgrounded shells and Monitors appear in no kill predicate and no kill helper on that path. The ESCAPE binding passes `suppressBackgroundAgentKill:true` and so never kills agents; the general Ctrl-C interrupt binding does NOT pass it (it is passed on Ctrl-C only in the viewing-an-agent screen branch). The double-press kill-agents chord kills within a 3000 ms window (`j4e=3000`), which is also the confirm toast's timeout.",
"depends": "csift's `idle-background-open` verdict treats an open shell or monitor as still open after an interrupt; assuming the interrupt closed everything produces a false `idle-eot`, and the resume path starts with an empty in-memory task list so survivors surface only as orphans.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "11-14",
"snippet": " /// The turn ended (a clean end_turn) but N background task(s) the lens counts have\n /// not returned - neither running nor stopped: by design (a dev server, a watcher)\n /// or not, csift cannot tell. Never satisfies `--until stop`.\n IdleBackgroundOpen,"
},
{
"path": "src/live/verdict.rs",
"lines": "358-367",
"snippet": " } else if eot_shape && background.open_counted() > 0 {\n notes.push(\n \"the turn ended, but background task(s) have not returned - by design (a dev \\\n server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n the next session start. `--background-since` / `--ignore-background` narrow \\\n what counts; kill a dead one with the tool or the shell\"\n .to_string(),\n );\n Verdict::IdleBackgroundOpen"
},
{
"path": "src/cli/live_args.rs",
"lines": "55-59",
"snippet": " PERSISTENT monitor never returns by design - name it with --ignore-background. NOT RETURNED IS NOT PROOF OF \\\n RUNNING: Claude Code's own orphan summary says a UI stop, a Monitor timeout or \\\n agent teardown leaves no transcript marker, and it reconciles only at the next \\\n session start. A long session commonly carries several to dozens of dangling \\\n or days-old tasks.\\n\\n\\"
}
],
"instrument": "Behavioural, on a live interactive session: launch a `run_in_background` sleep and an async agent, press the interrupt key, then check with `ps` that the shell is alive and that `csift status @<session> --format json | jq '.background'` still lists it open while the agent shows stopped. Only a live interactive session can produce this. Counting rule: one launch of each kind, one interrupt, three observations.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "AGENTS.md section 4; SPEC.md section 6 v0.10.0 ledger; SKILL.md status BACKGROUND TASKS paragraph; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -a -o -b 'suppressBackgroundAgentKill' ~/.local/share/claude/versions/2.1.258 (offsets 70769408, 180829503, 180831093, 180831371); dd if=<binary> bs=1 skip=180827000 count=8000 | tr -d '\\0' > kill_region.txt; rg -a -o 'function fr\\(.{0,200}' ~/.local/share/claude/versions/2.1.258 ; rg -a -o 'function fzn\\(.{0,240}' ~/.local/share/claude/versions/2.1.258",
"observed": "Kill predicate: `function she(T){return fr(T)&&(T.status===\"running\"||Dm(T))||T.type===\"in_process_teammate\"&&T.status===\"running\"}` with `function fr(e){return typeof e===\"object\"&&e!==null&&\"type\" in e&&e.type===\"local_agent\"}` and `function Dm(e){return e.status===\"completed\"&&Hk(e).size>0}`. The kill-all routine filters `Object.entries(po).filter(([,jn])=>she(jn))`; its teardown helper only touches local_agent (`function fzn(e,n,r=\"user\"){for(...)if(fr(d)&&Dm(d))WO(o,n,r);for(...)if(d.type===\"local_agent\"&&d.status===\"running\")WO(o,n,r)}`) and in_process_teammate. Escape: `je(\"chat:cancel\",()=>{...pn({gesture:\"escape\",suppressBackgroundAgentKill:!0})},{context:\"Chat\",...})`. Chord window: `var ihe=null,j4e=3000;` used as `if(Sr-oo.current<=j4e){...}` and as the confirm toast `timeoutMs:j4e` with text `Press ${...} again to stop background agents`. No killShell / KillShell / killBackground / shellRegistry / bgShells token occurs anywhere in the 8000-byte gesture-handler region.",
"rule": "One predicate, one filter site, one window constant. 'Kills type X' = X appears in the predicate the kill filter uses; 'does not kill X' = X appears in no kill predicate and no kill helper reachable from the interrupt handler within the extracted region.",
"note": "The mechanism half is decided by the binary. The live half - that a backgrounded shell process is still alive after the interrupt - was NOT reproduced here: no interrupt was pressed. What would decide it: in a live interactive session launch a run_in_background sleep and an async agent, press Ctrl-C, then check `ps` for the shell pid and `csift status @<session> --format json` for the two rows. One residual caveat: the interrupt path does call the wake-registry reset (clearing pending rescans and bumping a scan generation) in one branch, so whether an ARMED Monitor survives an interrupt is not settled by the kill predicate alone."
}
]
},
{
"id": "BG-027",
"area": "background-tasks",
"behavior": "On resume the harness rebuilds its background-task picture by SCANNING THE TRANSCRIPT, not from any persisted registry - there is no on-disk background-task store (~/.claude/tasks is the TODO store: 177 entries, 0 *.output files, per-session <n>.json todo objects). Session crons are re-registered and disk-resumable agents younger than 48 hours are handed to the auto-resume path; shells and workflows are only notified once and written a TERMINAL status (`stopped`), which the harness emits as a system task_notification event rather than as a jsonl record (0 such records in the project directory measured). So the orphan verdict leaves no transcript marker of its own, and a still-running shell from a previous process is carried forward marked stopped rather than running.",
"depends": "csift's whole-file transcript scan is the only instrument that survives a resume, and it is why the seventh verdict's note says Claude Code reconciles only at the next session start.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "358-367",
"snippet": " } else if eot_shape && background.open_counted() > 0 {\n notes.push(\n \"the turn ended, but background task(s) have not returned - by design (a dev \\\n server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n the next session start. `--background-since` / `--ignore-background` narrow \\\n what counts; kill a dead one with the tool or the shell\"\n .to_string(),\n );\n Verdict::IdleBackgroundOpen"
}
],
"instrument": "Launch a long `run_in_background` command, exit the session, resume it, then compare the terminal's task list (empty) with `csift status @<session> --format json | jq '.background.open'` (still lists the row) and `ps` (the process may still be alive). Counting rule: one launch, three observations.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SKILL.md status BACKGROUND TASKS paragraph; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python slice of orphan_region.txt around 'let f=Mn(c);$n(f)' and around 'function Fn(' / 'function On(' / 'function Mn('; rg -a -o 'function hs\\(.{0,500}' ~/.local/share/claude/versions/2.1.258 ; ls ~/.claude/tasks | wc -l; find ~/.claude/tasks -name '*.output' | wc -l; python dump of one ~/.claude/tasks/<session>/<n>.json; rg -a -o '\"subtype\":\"[a-z_]+\"' ~/.claude/projects/<one-project-dir> --glob '*.jsonl' | sort | uniq -c",
"observed": "Resume driver: `try{let f=Mn(c);$n(f),await En(f,n,o,r,i),Fn(f,n),On(f,n),bre(Mv(n.all()),i)}catch(f){h(f)}` where `Mn(e)` rebuilds the launch inventory by iterating the TRANSCRIPT records (`for(let A of e)if(A.type===\"assistant\"){...tool_use...}else if(A.type===\"user\"){...toolUseResult...}`). `$n({calls:e,results:n,deletedCronIds:o})` re-registers crons via `Z5({id,cron,prompt,createdAt,recur...})` under a `recurringMaxAgeMs` guard. `En` hands disk-resumable agents to the auto-resume path (`resume: handing ${I.length} disk-resumable orphaned agent(s) to the bg auto-resume path`). `Fn` (shells) and `On` (workflows) only notify and mark terminal via `hs(c.taskId,\"stopped\",{toolUseId:c.toolUseId,summary:it})`, where `function hs(e,n,r){...\\_u({type:\"system\",subtype:\"task_notification\",task_id:e,tool_use_id:...,status:n,output_file:...,summary:...})}`. Disk: ~/.claude/tasks holds 177 entries and 0 files named *.output; its contents are the TODO store (per-session directories of <n>.json with keys activeForm, blockedBy, blocks, description, id, status, subject, plus .lock and .highwatermark). In one project directory the subtype census over *.jsonl returns only stop_hook_summary, turn_duration, away_summary and compact_boundary - zero `\"subtype\":\"task_notification\"` lines (all 71 occurrences of the string task_notification in that directory are prose inside message content).",
"rule": "One read per function in the extracted orphan module; the disk side is one census of ~/.claude/tasks (entry count, *.output count, key set of one JSON file) and one scoped subtype census over one project directory (one observation per matching raw line).",
"note": "'Reported once as an orphan and then forgotten' is imprecise in one direction: the orphan is not dropped, it is written a terminal status in the in-process registry, so a task list shows it as stopped (wrongly, if the OS process is still alive) until eviction. The live half of the claim - the terminal task list being empty after a resume while ps still shows the process - was NOT reproduced here; what would decide it: launch a long run_in_background command, exit and resume the session, then compare the terminal's task list against `csift status @<session> --format json | jq '.background.open'` and `ps`. The csift snippet (src/live/verdict.rs:358-367) is verbatim in the current tree."
}
]
},
{
"id": "BG-028",
"area": "background-tasks",
"behavior": "The `<output-file>` a launch names is a real file on disk - for an async agent it is a SYMLINK to that agent's own transcript, for a shell a regular growing `.output` file - so its size and last-write time are an independent still-producing-output signal.",
"depends": "csift does one `stat` per open task and prints the bytes and the age; it is the only liveness evidence available for a task the harness never writes about again, and the agent symlink is why that file must not be read as an ordinary log.",
"code": [
{
"path": "src/live/background.rs",
"lines": "29-35",
"snippet": "//! At the next session start Claude Code reconciles orphans itself: one notification\n//! carrying several `<task-id>` tags plus `__orphan_summary__:shell`, status `stopped`,\n//! whose summary says the tasks \"may have been stopped (via the UI, Monitor timeout, or\n//! agent teardown - these leave no transcript marker)\". That sentence is the honesty\n//! bound: NOT RETURNED IS NOT PROOF OF STILL RUNNING. The `<output-file>` from the\n//! launch is a real file (an agent's is a symlink to its transcript); its size and\n//! mtime are an independent \"still producing output\" signal, one `stat` per open task."
},
{
"path": "src/live/background_scan.rs",
"lines": "340-353",
"snippet": "pub(crate) fn stat_output(t: &mut BgTask) {\n let Some(p) = t.output_file.as_deref() else {\n return;\n };\n let Ok(meta) = std::fs::metadata(p) else {\n return;\n };\n t.output_bytes = Some(meta.len());\n if let Ok(modified) = meta.modified() {\n if let Ok(age) = std::time::SystemTime::now().duration_since(modified) {\n t.output_age_secs = Some(i64::try_from(age.as_secs()).unwrap_or(i64::MAX));\n }\n }\n}"
}
],
"instrument": "`csift status @<session> --format json | jq '.background.tasks[] | {id, output_file, output_bytes, output_age_secs}'`, then `ls -l` the named path: an agent entry is a symlink into a `subagents/agent-*.jsonl`, a shell entry a regular `.output` file. Counting rule: one stat per open task.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 1; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python extraction of every <output-file> value from notif_raw.jsonl (the whole-corpus `csift search '' -t harness.notification --raw --max-count 0` dump) followed by os.path.islink / os.path.isfile / os.readlink / os.path.getsize on each; csift status @<session> --format json | (tasks[] | {kind,state,output_bytes,output_age_secs}) on two sessions with open shell tasks",
"observed": "1835 distinct <output-file> paths. On disk now: 92 symlinks, 46 regular files, 1697 gone (OS temp cleanup). 92/92 symlinks resolve, and 92/92 name exactly `agent-<task-id>.jsonl` - the launching agent's own transcript; their sizes read through the link range 88066 to 6572829 bytes. 46/46 regular files carry shell-shaped (36) or workflow-shaped (10) task ids, sizes 10 to 263769 bytes; zero agent-shaped ids are regular files and zero shell-shaped ids are symlinks. csift status on two sessions reports open shell rows with output_bytes=10/10/0 and output_age_secs=100979/100983/8381.",
"rule": "One stat per distinct <output-file> path harvested from the notification records. 'Symlink to that agent's own transcript' = os.readlink basename equals 'agent-' + the task id + '.jsonl'. Kind inferred from the task-id shape: a<16-17 hex> = agent, b<8-10 alnum> = shell, w-led = workflow.",
"note": "Holds, with two additions the claim does not make. (1) A workflow launch also gets a regular .output file, so 'agent = symlink, everything else = regular file' is the sharper rule. (2) The signal is only available while the OS temp tree survives: 1697 of 1835 named paths are already gone, so a missing output file is not evidence about the task. Separately, csift's own output_file was null on every agent-kind open row in the two sessions probed (it takes the path from the launch result text, which names one for shells), so in practice csift stats shell output files and not agent symlinks. Both snippets (src/live/background.rs:29-35, src/live/background_scan.rs:310-323) are verbatim in the current tree."
}
]
},
{
"id": "BG-029",
"area": "background-tasks",
"behavior": "A background task's `<output-file>` lives under the per-session scratch directory at `<scratch>/<encoded-project>/<session-uuid>/tasks/<task-id>.output`, not under `<claude-home>/tasks/`; that directory holds a shell's regular files and an agent's transcript symlinks side by side.",
"depends": "csift takes the path verbatim from the launch result text (after `written to: `) and stats it, so it never has to reconstruct the scratch location; a relocation of the store is transparent as long as the result text keeps naming the path.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "165-179",
"snippet": "pub(crate) fn after_marker(text: &str, marker: &str) -> Option<String> {\n let start = text.find(marker)? + marker.len();\n let rest = &text[start..];\n let end = rest\n .char_indices()\n .find(|&(i, c)| {\n c.is_whitespace()\n || c == ','\n || c == ')'\n || (c == '.' && rest[i + 1..].starts_with([' ', '\\n']))\n })\n .map_or(rest.len(), |(i, _)| i);\n let tok = rest[..end].trim_end_matches('.');\n (!tok.is_empty()).then(|| tok.to_string())\n}"
}
],
"instrument": "Launch a background command, read the path out of its result text, and `ls -la` that directory: it holds regular `.output` files and symlinks and is not under `~/.claude/tasks/`. Counting rule: one path per launch; the e2e test `rows_name_a_subagent_lane_and_a_live_output_file` in tests/cli/live/background.rs pins the stat path.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python census over the 1835 <output-file> values extracted from notif_raw.jsonl: segment count, leading-dash test on the project segment, uuid-shape test on the session segment, parent-directory basename, extension; plus ls ~/.claude/tasks | wc -l and find ~/.claude/tasks -name '*.output' | wc -l and a python dump of one ~/.claude/tasks/<session>/<n>.json",
"observed": "1835/1835 paths have exactly 7 path segments of the shape <os-temp-root>/claude-<uid>/<encoded-project>/<session-uuid>/tasks/<task-id>.output. Project segment starts with '-' (the encoded-cwd form): 1835/1835. Session segment matches the 8-4-4-4-12 uuid shape: 1835/1835. Parent directory basename == 'tasks': 1835/1835. Extension == '.output': 1835/1835. ~/.claude/tasks holds 177 entries and 0 files named *.output; a sample file there is a TODO object with keys activeForm, blockedBy, blocks, description, id, status, subject. Of the 8 tasks directories that still exist, 3 hold both a symlink and a regular file.",
"rule": "One path per distinct <output-file> value; the shape test is a per-segment predicate applied to every path. 'Side by side' = a directory whose surviving entries include at least one symlink and at least one regular file.",
"note": "Holds exactly as stated. The scratch root under the per-session directory is the OS temp tree, not ~/.claude, and ~/.claude/tasks is a different store entirely (the TODO list). csift takes the path verbatim from the launch result text via after_marker, so it never reconstructs this layout - src/live/background_scan.rs:165-179 is verbatim in the current tree, and the named e2e test exists at tests/cli/live/background.rs:404."
}
]
},
{
"id": "BG-030",
"area": "background-tasks",
"behavior": "A never-returned background launch is usually far from EOF but not always: over 88 never-returned launches in 63 top-level sessions the distance to EOF ranges 0.0 MB / 4 lines to 502.8 MB / 145674 lines (median 48.5 MB / 23854 lines), and 74 of 88 (84%) sit beyond the 512 KB tail window while 14 sit inside it. Shells alone top out at 375.5 MB and 109866 lines. Among launches that did return, 4.7% (84 of 1805 main-lane launches with a locatable completion) were more than 512 KB from their completion. 88 of 3849 corpus launches never returned at all (58 monitors, 24 shells, 6 agents), and 84 of the 88 were launched more than a day before their session's last record.",
"depends": "csift's background report is a whole-file scan behind a byte prefilter (measured +0.2-0.4 s worst case) rather than a tail read, because the 512 KB tail window covers none of the never-returned population; a tail-only implementation reports zero open tasks on any long session.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "21",
"snippet": "/// The five raw-byte needles (R13 law: bare value substrings, serialization-safe)."
}
],
"instrument": "For each never-returned launch compute (file length - byte offset of the launch line) and (total lines - launch line number). Counting rule: one distance per launch; a launch is never-returned when no `<task-notification>` carrier anywhere under the same session root names its tool_use id or its backgroundTaskId.",
"located": {
"claude_code": "2.1.252",
"csift": "0.10.0",
"source": "SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift list --format json --max-count 0 --no-subagents (63 distinct top-level sessions); for each: csift status @<id> --format json > statusjson/<id>.json; python aggregation of background.{open,completed,failed,killed,stopped,timed_out} and background.tasks[]; then for each open task a python locate of its tool_use_id in the launching file (main transcript first, then that session's subagent lanes) recording byte offset, line number, file size and total line count; and a separate single-pass python scan of the 66 main transcripts joining launches to <task-notification> carriers by tool-use-id",
"observed": "63 top-level sessions, 3849 launches (sum of the six state counters), states {completed 3542, failed 110, killed 82, open 88, timed_out 19, stopped 8}. All 88 open (never-returned) launches located. Distance to EOF, all kinds: min 0.0 MB / 4 lines, median 48.5 MB / 23854 lines, max 502.8 MB / 145674 lines; 74 of 88 sit beyond the 512 KB tail window, 14 sit inside it. Shells alone (n=24): min 0.0 MB / 4 lines, median 31.5 MB / 4336 lines, max 375.5 MB / 109866 lines; 13 of 24 beyond 512 KB. Agents (n=6): 6 of 6 beyond 512 KB. Monitors (n=58): 55 of 58 beyond 512 KB. Age: 84 of 88 open launches were launched more than a day before their session's last timestamped record (shells 23 of 24). Returned launches, main lanes only: 84 of 1805 = 4.7% sat more than 512 KB from their completion.",
"rule": "A launch is never-returned when csift 0.10.0 status reports its state as `open` (no completion carrier names its tool_use id or its background task id). Total launches = the sum of the six state counters over all 63 top-level sessions. Distance to EOF = (file size in bytes - byte offset of the launch line) and (total lines - launch line number) in the file the launch was issued from. Age = seconds between the task's launched_utc and the newest timestamp in the session's main transcript. The returned-launch distance is a separate cruder pass: launches and carriers taken from the 66 main transcripts only, joined by tool-use-id, distance = bytes from the launch line to the nearest following carrier.",
"note": "The ledger's two upper numbers reproduce exactly on the shell subgroup (375 MB and 109866 lines), which is how the original was almost certainly scoped; its lower bounds (56 MB, 4545 lines) do not survive a full census, and 'sits far from EOF' has 14 counterexamples inside the tail window. Its '24 of 3133 launches never returned' is a shells-only count: 24 shells still, but out of 3849 launches now, with 88 never-returned across all kinds. The load-bearing conclusion is untouched - a 512 KB tail read would miss 84% of the never-returned population - and the tail constant snippet (src/live/tail.rs:14-16) is verbatim in the current tree."
}
]
},
{
"id": "BG-031",
"area": "background-tasks",
"behavior": "The harness writes NOTHING about a still-running background SHELL at end of turn: the 2.1.258 turn_duration record factory emits only durationMs, budgetTokens, budgetLimit, budgetNudges, messageCount, pendingBackgroundAgentCount and pendingWorkflowCount, and neither pending count can ever include a shell - the counter buckets a live task into pendingAgents only when it is a non-main-session local_agent with isBackgrounded set, and into pendingWorkflows only when its type is local_workflow, while a background shell is type local_bash and a Monitor is monitor_mcp/monitor_ws. Across 4637 turn_duration records on this corpus the 9 observed key sets contain no shell field of any kind. Such records ARE written while background work is open: on a replay of 4638 observations, 4375 had at least one background shell open, 1920 at least one async agent, 2232 at least one async workflow.",
"depends": "csift never reads an end-of-turn telemetry record as proof the session is done and scans the whole main transcript for launches and completions instead; this hole is exactly what the `idle-background-open` verdict fills.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "354-367",
"snippet": " } else if running_shape {\n Verdict::Running\n } else if children.live_count > 0 {\n Verdict::WaitingChildren\n } else if eot_shape && background.open_counted() > 0 {\n notes.push(\n \"the turn ended, but background task(s) have not returned - by design (a dev \\\n server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n the next session start. `--background-since` / `--ignore-background` narrow \\\n what counts; kill a dead one with the tool or the shell\"\n .to_string(),\n );\n Verdict::IdleBackgroundOpen"
},
{
"path": "src/live/background.rs",
"lines": "292-306",
"snippet": " let mut pos = 0usize;\n while pos < bytes.len() {\n let end = memchr::memchr(b'\\n', &bytes[pos..]).map_or(bytes.len(), |i| pos + i);\n let line = &bytes[pos..end];\n pos = end + 1;\n if !line_is_bg_candidate(line) {\n continue;\n }\n let Ok(Some(rec)) = crate::parse::parse_line(line) else {\n continue;\n };\n ingest_launches(&rec, &lane, &mut tasks);\n if is_main {\n ingest_carriers(&rec, &mut carriers, &mut notes);\n }"
},
{
"path": "src/live/verdict.rs",
"lines": "309-314",
"snippet": " let eot_shape = main_tail.unreturned_use.is_none()\n && (main_tail\n .last_stop_reason\n .as_deref()\n .is_some_and(|s| s == \"end_turn\")\n || registry_shell);"
}
],
"instrument": "Replay a transcript maintaining the set of `run_in_background` launches with no `<task-notification>` naming them yet; at each `turn_duration` line record that set's size and the record's key set. Counting rule: one observation per `turn_duration` record, \"open at that instant\" meaning launched earlier in the file with no completion carrier seen; live-side, `csift search \"\" @<session> -t harness.meta.turn-duration --max-count -1 --no-truncate` shows no shell field while `csift status @<session>` reports `idle-background-open` with a shell row.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,160}pendingBackgroundAgentCount.{0,160}' (2) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function gje\\({tasks.{0,600}' (3) a python pass over every *.jsonl under ~/.claude/projects (7791 files) that (a) key-set-censuses every record with type==\"system\" and subtype==\"turn_duration\" and (b) walks each transcript in line order maintaining the set of background launches with no completion carrier yet, sampling that set at each turn_duration record",
"observed": "Binary record factory: function B_t(e,n,r,o,d){return{type:\"system\",subtype:\"turn_duration\",durationMs:e,budgetTokens:n?.tokens,budgetLimit:n?.limit,budgetNudges:n?.nudges,messageCount:r,pendingBackgroundAgentCount:o,pendingWorkflowCount:d,timestamp:new Date().toISOString(),uuid:oT(),isMeta:!1}} - no shell field of any kind. Binary counter: function gje({tasks:l,queuedCommands:p=[]}){...y=(M)=>{if(GE(M)&&M.isBackgrounded)g.add(M.id);else if(M.type===\"local_workflow\")T.add(M.id)};for(let M of Object.values(l))if(M.status===\"running\"||Fs(M.status)&&!M.notified)y(M);...return{pendingAgents:g.size,pendingWorkflows:T.size}} with function GE(e){return fr(e)&&e.agentType!==\"main-session\"} - a background shell is task type local_bash and falls in neither bucket. Corpus: 4637 turn_duration records in 54 files, 9 distinct key sets, key union = cwd, durationMs, entrypoint, gitBranch, isMeta, isSidechain, messageCount, parentUuid, pendingBackgroundAgentCount, pendingWorkflowCount, sessionId, sessionKind, slug, subtype, timestamp, type, userType, uuid, version (19 keys, none shell-related). Replay over 4638 observations: 4375 (94.3%) had >=1 open background shell, 1920 (41.4%) had >=1 open async agent, 2232 (48.1%) had >=1 open async workflow.",
"rule": "One observation per turn_duration record. 'Open at that instant' = a launch seen earlier in the SAME transcript with no later <task-notification> in that transcript naming its <tool-use-id> or its <task-id>. Shell launch = a Bash/PowerShell tool_use whose input.run_in_background is true; async agent = an Agent/Task tool_use with run_in_background true, or a toolUseResult with status==\"async_launched\" carrying an agentId; async workflow = a toolUseResult with status==\"async_launched\" carrying a taskId. Key-set census counts one key set per record.",
"note": "The behaviour holds and is now sourced, not merely sampled: the binary's own record factory and pending-count function prove the shell exclusion for every turn_duration record, not just the 100 the claim sampled. The claim's population (628) is not reproducible here - a whole-corpus scan finds 4637 such records in 54 files and no single project directory yields 628 - and the two ratios are far higher than claimed (94% vs 16% for an open shell, 41% vs 8% for an open async agent), so treat 100/628 and 51/628 as artefacts of an unstated scope, not as a rate. One forward-looking gap: 2.1.258 also emits budgetTokens, budgetLimit and budgetNudges on this record (undefined values are dropped by JSON.stringify, so none appear in this corpus), and the terminal renderer additionally reads briefHiddenCount; csift's Record models none of the four. The corpus is live - a second pass 15 minutes later saw 4638 records."
}
]
},
{
"id": "BG-032",
"area": "background-tasks",
"behavior": "pendingBackgroundAgentCount on a turn_duration record is never emitted as the literal 0 - the emitter writes `k>0?k:void 0` so a zero count becomes undefined and is dropped from the JSON, and absence of the key therefore means zero. The field does NOT track a transcript-derived set of launched-but-unreturned agents: it is a snapshot of the harness's live in-memory task map (a task counts when its status is running, or when it is terminal but not yet notified), so a transcript replay disagrees with it in both directions - on this corpus the field matched the replayed open-agent set in only 25 of the 136 records that carry it, reading LOWER than the replay 102 times and HIGHER 9 times.",
"depends": "any consumer reading the field must treat an absent key as 0 rather than as unknown; the residual under-count comes from agents that notified and were resumed (a notification is not terminal) and from agents spawned in a child lane.",
"code": [
{
"path": "src/model/record.rs",
"lines": "177-184",
"snippet": " /// `turn_duration`: background agents still running at turn end (the REPL's\n /// \"Waiting for N agents\" line). Optional; measured on ~3% of records.\n #[serde(default, rename = \"pendingBackgroundAgentCount\")]\n pub pending_background_agent_count: Option<serde_json::Value>,\n\n /// `turn_duration`: workflows still running at turn end. Optional (~5%).\n #[serde(default, rename = \"pendingWorkflowCount\")]\n pub pending_workflow_count: Option<serde_json::Value>,"
},
{
"path": "src/search/record_text.rs",
"lines": "197-209",
"snippet": " for (key, v) in [\n (\"messageCount\", rec.message_count.as_ref()),\n (\n \"pendingBackgroundAgentCount\",\n rec.pending_background_agent_count.as_ref(),\n ),\n (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),\n ] {\n if let Some(n) = Record::u64_field(v) {\n fields.push(format!(\"{key}={n}\"));\n }\n }\n (!fields.is_empty()).then(|| format!(\"[turn duration: {}]\", fields.join(\" \")))"
}
],
"instrument": "`rg -c '\"pendingBackgroundAgentCount\":\\s*0' ~/.claude/projects --glob '*.jsonl'` returns no matches while `rg -c '\"pendingBackgroundAgentCount\"'` returns many. Counting rule: one matching line = one record.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "SPEC.md section 6 v0.10.0 ledger; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}pendingAgents.{0,500}' (2) a python census of every type==\"system\" subtype==\"turn_duration\" record under ~/.claude/projects, tabulating the presence and value of pendingBackgroundAgentCount (3) the same replay as BG-031, comparing the field against the replayed open async-agent set",
"observed": "Binary emitter: if(k>0||x>0)return{durationMs:g,pendingBackgroundAgentCount:k>0?k:void 0,pendingWorkflowCount:x>0?x:void 0,...}; return{durationMs:...,pendingBackgroundAgentCount:void 0,pendingWorkflowCount:void 0,...} - the zero case is literally undefined, which JSON.stringify omits. Corpus: 136 of 4637 turn_duration records (2.93%) carry the key; observed values 1:59, 2:32, 3:17, 4:9, 5:5, 6:2, 7:4, 8:2, 9:2, 10:1, 11:1, 12:1, 13:1 - minimum 1, zero occurrences of the value 0. Agreement with the replay: on the 136 key-bearing records, field==replay 25, field<replay 102, field>replay 9; counting an absent key as 0 over all 4638 observations, match 2741, field<replay 1888, field>replay 9.",
"rule": "One record = one observation. 'Never 0' is decided on parsed values, not on a byte pattern, so it is immune to JSON spacing. Agreement compares the field (absent read as 0) against the count of open async agents by the BG-031 rule at that record.",
"note": "First half of the claim is now proven at the source, not merely sampled: the ternary `k>0?k:void 0` is why no literal 0 exists, so a consumer reading an absent key as 'unknown' rather than 0 is wrong at every version that ships this emitter. The second half does not survive its own counting rule: 608 of 628 does not reproduce (25 of 136 on key-bearing records, 2741 of 4638 counting absence as zero), and mismatches are not one-directional - 9 observations have the field ABOVE the replay. The mechanism explains both: the field counts a live in-process map that dies with the session process, while a transcript replay accumulates launches that no notification ever closed (a long session carries dozens of dangling arms), so the replay drifts upward across resumes; conversely a task that already notified but is still running stays counted by the harness while the replay has closed it, which is the 9-case direction the original claim described."
}
]
},
{
"id": "BG-033",
"area": "background-tasks",
"behavior": "ScheduleWakeup mints NO task id at all. At 2.1.258 it takes {delaySeconds, prompt, reason, noop} - noop is a fourth REQUIRED field ('`noop` is required when `stop` is not true'), true meaning the tick changed nothing so consecutive ticks collapse in the terminal - or the {stop:true} form; its toolUseResult is {clampedDelaySeconds, scheduledFor, wasClamped}, plus {cancelledWakeups, stopped} on the stop form. delaySeconds is clamped to [60, 3600]. A scheduled wakeup can never be joined to a later notification by id.",
"depends": "csift's background scanner does not model `ScheduleWakeup`; its cadence pulses reach the taxonomy only as `harness.schedule.wakeup` / `harness.notification.monitor` records, never as background tasks.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "21-39",
"snippet": "/// The five raw-byte needles (R13 law: bare value substrings, serialization-safe).\npub(crate) fn line_is_bg_candidate(line: &[u8]) -> bool {\n static FINDERS: std::sync::LazyLock<Vec<memmem::Finder<'static>>> =\n std::sync::LazyLock::new(|| {\n [\n &b\"run_in_background\"[..],\n b\"Command running in background\",\n b\"async_launched\",\n b\"task-notification\",\n b\"stopped by the user\",\n b\"\\\"Monitor\\\"\",\n b\"Monitor started\",\n ]\n .into_iter()\n .map(memmem::Finder::new)\n .collect()\n });\n FINDERS.iter().any(|f| f.find(line).is_some())\n}"
}
],
"instrument": "`rg -c '\"name\":\"ScheduleWakeup\"' ~/.claude/projects --glob '*.jsonl'`, then enumerate the paired results' `toolUseResult` key sets - no id-shaped field appears. Counting rule: one key-set observation per result record, one row per `ScheduleWakeup` tool_use block.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}clampedDelaySeconds.{0,300}' (2) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}noop:.{0,400}' (3) a python pass over the 4 transcripts under ~/.claude/projects that contain \"name\":\"ScheduleWakeup\", tabulating every ScheduleWakeup tool_use input key set and the toolUseResult key set of every record carrying the paired tool_result block",
"observed": "Binary output schema: c({scheduledFor:A().describe(\"Epoch ms timestamp when the next wakeup will fire\"),clampedDelaySeconds:A().describe(\"Actual delay used after clamping to runtime bounds\"),wasClamped:M().describe(\"True if the requested delaySeconds was outside [60, 3600]\"),stopped:M().optional(),cancelledWakeups:A().optional()}) - no id-shaped field. Binary call path: if(d===!0)return{data:{scheduledFor:0,clampedDelaySeconds:0,wasClamped:!1,stopped:!0,cancelledWakeups:yYn()}} plus the guards '`delaySeconds` and `reason` are required when `stop` is not true.', '`prompt` is required when `stop` is not true.', '`noop` is required when `stop` is not true.'. Corpus: 562 ScheduleWakeup tool_use blocks - 561 with input keys {delaySeconds, prompt, reason} and 1 with {stop}; 560 paired results - 559 with toolUseResult keys {clampedDelaySeconds, scheduledFor, wasClamped} and 1 with {cancelledWakeups, clampedDelaySeconds, scheduledFor, stopped, wasClamped}. No key in any of the 562 inputs or 560 results is an id.",
"rule": "One row per ScheduleWakeup tool_use block (keyed by block id); one key-set observation per record whose message.content carries a tool_result whose tool_use_id is one of those ids.",
"note": "The load-bearing half - no id, so no id join - is confirmed twice: by the binary's declared output schema and by 560 real paired results, none of which carries an id-shaped key. The input list needed a fourth field: 2.1.258 requires `noop` unless stop is true, and none of the 562 recorded blocks carries it because they predate that field. Also newly pinned from the binary: delaySeconds is clamped to [60, 3600] and wasClamped reports the clamp."
}
]
},
{
"id": "BG-034",
"area": "background-tasks",
"behavior": "Claude Code's task tools persist ONE JSON file per task under a per-session directory in <claude-home>/tasks/, and TWO directory-name forms exist on real disks: the full session uuid (16 here) and the newer session-<first 8 uuid chars> form (161 here). Each file carries {id, subject, description, status, blocks, blockedBy} with STRING ids and list-valued blocks/blockedBy; activeForm is OPTIONAL (absent on 86 of 402 files) and two further optional keys are observed in the wild, owner (36 files) and metadata (26 files). The status set is OPEN - pending, in_progress and completed observed.",
"depends": "csift reads BOTH directory forms and merges them, renders anything that is not `completed` as an open row with its verbatim status (in_progress first, with blockers) and folds completed ones to a count; probing only one form silently reports no tasks for a session using the other, and a numeric-id assumption or a closed status enum would drop rows.",
"code": [
{
"path": "src/live/tasks.rs",
"lines": "1-10",
"snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a\n//! per-session directory. Two directory-name forms exist on real disks (both verified):\n//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file\n//! carries `{id, subject, description, activeForm, status, blocks, blockedBy}` with\n//! string ids. The set of `status` values is OPEN (pending / in_progress / completed\n//! observed); anything that is not `completed` renders as an open row with its verbatim\n//! status. This is a live-truth read (current values only, no history) - the same\n//! carve-out `status` itself lives under."
},
{
"path": "src/live/tasks.rs",
"lines": "37-41",
"snippet": " let tasks_root = home.join(\"tasks\");\n let mut dirs = vec![tasks_root.join(owner_id)];\n if let Some(prefix) = owner_id.get(..8) {\n dirs.push(tasks_root.join(format!(\"session-{prefix}\")));\n }"
}
],
"instrument": "`ls ~/.claude/tasks | sed 's/^session-.*/session-form/' | sort | uniq -c` shows both forms; `jq -r .status ~/.claude/tasks/*/*.json | sort | uniq -c` gives the status census; `csift status @<session> --format json | jq '{tasks, tasks_completed}'` prints the rows (null = no dir, [] = a dir with nothing). Counting rule: one row per non-completed task file across both directory forms.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "AGENTS.md section 1; SPEC.md section 6.13; SPEC.md section 6 v0.9.4 ledger; CHANGELOG 0.9.4; src/live/tasks.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) ls ~/.claude/tasks | sed 's/^session-.*/session-form/' | sed -E 's/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/uuid-form/' | sort | uniq -c (2) a python census of every ~/.claude/tasks/*/*.json: key set, status value, python type of id, blocks and blockedBy (3) csift status @<8-char session prefix> --format json for a session that owns BOTH a uuid-form and a session-form directory, compared with the on-disk file counts",
"observed": "Directory forms: 161 session-form, 16 uuid-form (177 directories). 402 task files, 5 distinct key sets: 258 {activeForm, blockedBy, blocks, description, id, status, subject}; 82 the same minus activeForm; 32 with an extra owner; 26 with an extra metadata; 4 minus activeForm plus owner. id is a str in 402 of 402; blocks and blockedBy are lists in 402 of 402. Status census: completed 272, pending 99, in_progress 31. Merge check on one session: 7 files in its uuid-form directory + 21 in its session-form directory = 28; csift status reported 10 open rows + tasks_completed 18 = 28, and the disk census for those 28 was completed 18, in_progress 1, pending 9. An empty session-form directory reported tasks [] with tasks_completed 0.",
"rule": "One row per *.json file under a tasks directory; a directory counts once per name form; open row = any file whose status is not 'completed', across both directory forms for the owning session.",
"note": "Both directory forms are real and csift merges them: the one session on this disk that owns both forms has 7 + 21 files and csift status reports exactly 10 open + 18 completed. Two corrections to the shape: activeForm is not universal (86 of 402 files lack it) and the file can carry owner or metadata, so a fixed 7-key expectation is wrong even though a strict reader would survive it. The string-id and open-status halves hold exactly (402 of 402 string ids; three status values observed)."
}
]
},
{
"id": "BG-035",
"area": "background-tasks",
"behavior": "Neither live store carries background-task state: the session registry rows contain no `monitor` or `taskId` field, and `<claude-home>/tasks/` holds only the task-list files, never a 9-character `b`-led background task id.",
"depends": "csift reconstructs background tasks from the transcript alone and reads `<claude-home>/tasks/` only as the task-list section; a future registry that did carry live task state would let the whole-file scan be replaced by a cheap read.",
"code": [
{
"path": "src/live/tasks.rs",
"lines": "1-10",
"snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a\n//! per-session directory. Two directory-name forms exist on real disks (both verified):\n//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file\n//! carries `{id, subject, description, activeForm, status, blocks, blockedBy}` with\n//! string ids. The set of `status` values is OPEN (pending / in_progress / completed\n//! observed); anything that is not `completed` renders as an open row with its verbatim\n//! status. This is a live-truth read (current values only, no history) - the same\n//! carve-out `status` itself lives under."
},
{
"path": "src/live/background.rs",
"lines": "37-39",
"snippet": "//! Never-returned launches sit 56-375 MB before EOF on real files, so this is a whole-\n//! file scan behind a five-needle byte prefilter (measured +0.2-0.4 s worst case), not a\n//! tail read."
}
],
"instrument": "`grep -ril 'monitor\\|taskId' ~/.claude/sessions/` and `grep -rlE '\"b[a-z0-9]{8}\"' ~/.claude/tasks/` - both empty. Counting rule: one hit = one file; zero files on either side is the expected observation.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "grep -ril 'monitor\\|taskId' ~/.claude/sessions/ ; grep -rlE '\"b[a-z0-9]{8}\"' ~/.claude/tasks/ ; plus a python key-set census of every parseable file in ~/.claude/sessions and, as a control, rg -o '<task-id>[a-z0-9]{9}</task-id>' over ~/.claude/projects",
"observed": "0 files matched in ~/.claude/sessions (14 entries) and 0 files matched under ~/.claude/tasks (402 task files). The registry key sets are {bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version} and small variants of it, plus token sidecars {peerToken, pidDomain, procStart} - no monitor key, no taskId key. Control: b-led 9-character task ids are abundant in the transcripts (14198 of 14715 nine-character <task-id> values start with b), so the probe would have fired had such an id been persisted.",
"rule": "One hit = one file; zero files on either side is the expected observation. The control counts one occurrence per <task-id> match, bucketed by first character.",
"note": "Both stores are silent about background tasks, and the control rules out a vacuous probe: the id shape the grep looks for is the dominant one in the transcripts (14198 b-led of 14715 nine-character task ids) yet appears in none of the 402 task files, and none of the 14 registry entries carries a monitor or taskId key. The consequence for csift stands - the transcript scan is the only background instrument, and <claude-home>/tasks/ is only the task-list section."
}
]
},
{
"id": "BG-036",
"area": "background-tasks",
"behavior": "Only a BACKGROUND session (`CLAUDE_CODE_SESSION_KIND == \"bg\"`) writes a live in-flight registry, at `~/.claude/jobs/<short-id>/state.json` carrying `inFlight:{tasks, queued, kinds[], drainableMonitors, wake?}`; an interactive session returns early and writes nothing there.",
"depends": "csift's live layer does not read that file, so for an INTERACTIVE session the transcript scan is the only background instrument; it is also the only place a live-armed signal exists if csift ever adds one.",
"code": [
{
"path": "src/live/background.rs",
"lines": "37-39",
"snippet": "//! Never-returned launches sit 56-375 MB before EOF on real files, so this is a whole-\n//! file scan behind a five-needle byte prefilter (measured +0.2-0.4 s worst case), not a\n//! tail read."
}
],
"instrument": "`cat ~/.claude/jobs/*/state.json` - an interactive-only machine shows `{\"inFlight\":{\"tasks\":0,\"queued\":0,\"kinds\":[],\"drainableMonitors\":0}}` or no directory at all; the `kinds` array carries `monitor` / `monitor_ws` only for a background session. Counting rule: one file per background job directory.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}CLAUDE_CODE_SESSION_KIND.{0,200}' (2) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}inFlight:.{0,260}' (3) find ~/.claude/jobs -type f and cat of the one state.json found",
"observed": "Binary writer gate: async function bre(e,n){let r=a.CLAUDE_JOB_DIR;if(!r||a.CLAUDE_CODE_SESSION_KIND!==\"bg\")return; ... await As(r,{...o,inFlight:{tasks:e.count,queued:d.queued,kinds:[...e.kinds],...e.drainableMonitors!==void 0&&e.drainableMonitors>0&&{drainableMonitors:e.drainableMonitors},...d.wake!==void 0&&{wake:d.wake}},updatedAt:...}). Persisted schema: inFlight:c({tasks:A(),queued:A(),kinds:R(i()),drainableMonitors:A().int().nonnegative().optional(),wake:c({at:A().optional(),reason:i().optional(),fires:A().int().nonnegative(),keepalive:x(!0).optional()}).optional().catch(void 0)}).optional(). Containment check: let r=Ce(Se(),\"jobs\")+ve; if(!o.startsWith(r))return!1 - the job directory must sit under <claude-home>/jobs/. Disk: exactly one job directory, named by an 8-character short id, holding state.json and timeline.jsonl; its state.json carries \"inFlight\":{\"tasks\":0,\"queued\":0,\"kinds\":[],\"drainableMonitors\":0} and \"template\":\"bg\". No other session on this machine (14 registry entries, 15 project directories) has a jobs directory.",
"rule": "One file per background job directory; the gate is decided by reading the writer's early return, not by absence alone.",
"note": "The gate is proven at the source rather than inferred from an empty directory: the writer returns immediately unless CLAUDE_JOB_DIR is set AND CLAUDE_CODE_SESSION_KIND == \"bg\", and the job directory is separately validated to sit under <claude-home>/jobs/. The claimed inFlight shape matches the persisted schema exactly, with two refinements worth knowing: drainableMonitors is itself optional (2.1.258 writes it only when greater than zero, so the observed zero on disk was written by an earlier build), and wake is an object {at?, reason?, fires, keepalive?}. The kinds array is populated from a task census that can push monitor, monitor_ws and session_cron; the one file on this machine has an empty kinds array, so the monitor entries themselves are not observed here."
}
]
},
{
"id": "BG-037",
"area": "background-tasks",
"behavior": "A BACKGROUND SESSION lane is self-identifying on disk: records written by a session whose kind is bg carry \"sessionKind\":\"bg\", and the marking is inherited by the subagent transcripts that session spawns. It does NOT mark asynchrony: on this corpus all 6225 occurrences belong to one background-job session (951 on its own top-level transcript, 5274 across its 20 subagent transcripts), while the 150 asynchronous launches made from interactive sessions produced child transcripts carrying no sessionKind at all. The value set is not a singleton either - the binary accepts bg, daemon and daemon-worker; bg is simply the only kind this machine has recorded.",
"depends": "csift's child-lane classifier does not read it - an async agent is treated as an ordinary child lane by tail shape alone - so `sessionKind` is an available but unused discriminator if async-versus-sync ever needs distinguishing.",
"code": [
{
"path": "src/live/children.rs",
"lines": "46-56",
"snippet": "pub(crate) fn children_report(\n main_jsonl: &Path,\n returned: &std::collections::HashSet<String>,\n) -> Result<ChildrenReport> {\n let mut report = ChildrenReport::default();\n let subs = crate::subagent::subagent_transcript_files(main_jsonl).unwrap_or_default();\n for sub in &subs {\n let sid = crate::subagent::session_id_from_path(sub);\n let shape = tail_shape(sub)?;\n // The RECORD-tail instant is the semantic fact (mtime can lag or lead it).\n let tail_age = shape.last_ts_utc.as_deref().and_then(|t| age_secs(Some(t)));"
}
],
"instrument": "`rg -o '\"sessionKind\":\"[^\"]*\"' ~/.claude/projects --glob '*.jsonl' | sort | uniq -c` expects one distinct value. Counting rule: one occurrence per matching record; the files carrying it are all `subagents/agent-*.jsonl`.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) rg -o '\"sessionKind\":\"[^\"]*\"' ~/.claude/projects --glob '*.jsonl' -N --no-filename | sort | uniq -c (2) rg -l '\"sessionKind\":\"bg\"' over the same tree, grouped by owning session and by file shape (3) rg -l 'async_launched' over the same tree (4) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,150}sessionKind.{0,150}'",
"observed": "6225 occurrences, all \"bg\" - one distinct value. But the carriers are 21 files that ALL belong to ONE background-job session: its own top-level transcript (951 occurrences) and its 20 built-in subagent transcripts (5274 occurrences). Zero occurrences under any subagents/workflows/** path, and zero in any transcript of an interactive session, even though 150 async_launched launches are spread across many interactive sessions in the same corpus. Binary: function Ij(){let e=a.CLAUDE_CODE_SESSION_KIND;if(e===\"bg\"||e===\"daemon\"||e===\"daemon-worker\")return e;return} and the resume-list reader U=B===\"bg\"||B===\"daemon\"||B===\"daemon-worker\"?B:void 0 - three admissible values, not one.",
"rule": "One occurrence per matching record; files grouped by the session directory that owns them and by whether the path contains a subagents/ component and a workflows/ component.",
"note": "The value census survives (6225 of 6225 are bg) but the attribution does not: the field tracks the SESSION's kind, not whether a lane was launched asynchronously. The decisive counter-observation is that async launches exist in many interactive sessions here (150 async_launched results) and none of their child transcripts carries the field, while every child of the single bg-job session does. Anyone using sessionKind as an async-versus-sync discriminator would misclassify every async lane started from an interactive session, and a reader treating bg as the only value will break on a daemon or daemon-worker session."
}
]
},
{
"id": "BG-038",
"area": "background-tasks",
"behavior": "A background task can be designed never to return (a dev server, a watcher, a persistent monitor), so no completion record will ever arrive for it and `not returned` can never be read as `still running`.",
"depends": "csift's `wait` REQUIRES `--timeout` and exits 124 on expiry with an at-exit report - the single documented exception to its zero-or-non-zero exit contract - and the `idle-background-open` verdict is neither running nor stopped, so it never satisfies `--until stop`, which admits only idle-eot and stale-dead (a process proven dead still stops the wait with a task open).",
"code": [
{
"path": "src/live/wait.rs",
"lines": "36-42",
"snippet": " // The timeout is REQUIRED (v0.10.0): a background task may never return by design\n // (a dev server, a watcher), so an unbounded wait on `stop` is a bug, not a wait.\n let Some(timeout_secs) = args.timeout else {\n bail!(\n \"wait needs --timeout <SECS>: a background task can be designed never to return \\\n (a dev server, a watcher), so a wait without a bound never ends. Pick a bound, \\\n branch on exit 124, and read the at-exit report; narrow what counts with \\"
},
{
"path": "src/live/wait.rs",
"lines": "206-218",
"snippet": "/// The timeout elapsed: render `fired:\"timeout\"` + exit 124 (never through the error\n/// path - a timeout is a NORMAL monitor outcome, just a distinguishable one).\nfn finish_timeout(\n args: &WaitArgs,\n main: &Path,\n lens: &BackgroundLens,\n activity: &Activity,\n waited: std::time::Duration,\n) -> Result<()> {\n let assessment = assess_path(main, args.want_subagents(), lens)?;\n emit_wait(args, \"timeout\", main, &assessment, activity, waited)?;\n let _ = std::io::stdout().flush();\n std::process::exit(i32::from(TIMEOUT_EXIT));"
},
{
"path": "src/live/conditions.rs",
"lines": "198-206",
"snippet": "/// Match a fresh assessment against the verdict-class conditions.\npub(crate) fn verdict_matches(cond: &Cond, verdict: Verdict) -> bool {\n match cond {\n Cond::Stop => matches!(verdict, Verdict::IdleEot | Verdict::StaleDead),\n Cond::Hitl => verdict == Verdict::WaitingHitl,\n Cond::VerdictIs(v) => verdict == *v,\n _ => false,\n }\n}"
}
],
"instrument": "`csift wait @<session> --until stop` without `--timeout` expects a pointed refusal naming the reason; then `csift wait @<session> --until stop --timeout 3; echo $?` expects 124. Counting rule: one invocation per branch, exit code observed.",
"located": {
"claude_code": "2.1.252",
"csift": "0.10.0",
"source": "AGENTS.md section 4; SPEC.md section 6 v0.10.0 ledger; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift wait @<8-char session prefix> --until stop; echo $? then csift wait @<same prefix> --until stop --timeout 3; echo $? plus csift status @<same prefix> --format json for the open-task census",
"observed": "Without --timeout: 'csift: error: wait needs --timeout <SECS>: a background task can be designed never to return (a dev server, a watcher), so a wait without a bound never ends. Pick a bound, branch on exit 124, and read the at-exit report; narrow what counts with --background-since now / --ignore-background <RE>', exit 1. With --timeout 3: exit 124, at-exit report 'fired timeout / verdict waiting-children / waited 4s', and a background line '12 open; 1079 completed, 10 failed, 3 killed, 1 stopped'. The 12 open arms are 61 to 91 days old and are re-arm loops, watch loops and monitors - launches for which no completion record will ever arrive. Code: Cond::Stop => matches!(verdict, Verdict::IdleEot | Verdict::StaleDead) in src/live/conditions.rs:190, so IdleBackgroundOpen never satisfies --until stop.",
"rule": "One invocation per branch, exit code observed; open background arms counted once per launch with no completion carrier, ages taken from the launch instant.",
"note": "Both branches ran: the refusal names the reason and exits 1, and the bounded wait exits 124 with the at-exit report. The premise is corroborated on real data rather than assumed - the session used carries 12 background arms open for 61 to 91 days against 1079 completed, which is what 'designed never to return' looks like on disk, and matches Claude Code's own position that a UI stop, a Monitor timeout or agent teardown leaves no transcript marker. The 'never satisfies --until stop' half is settled by the condition table, which admits only idle-eot and stale-dead."
},
{
"claude_code": "2.1.258",
"csift": "0.10.2",
"date": "2026-09-03",
"verdict": "refined",
"instrument": "python scan of src/ for string literals carrying interior runs of eight or more spaces, and the installed csift 0.10.1 run as `wait @<id> --until stop` without --timeout",
"observed": "two literals (the wait.rs timeout guard message and the recover replay.rs bash-append boundary detail) carried 14- and 34-space runs where line-continuation backslashes had been lost; 0.10.1 printed `never to return (a dev server`; the guard, exit 1 and exit 124 behaved as claimed",
"rule": "a run of spaces inside a literal that is no padding format is a lost continuation",
"note": "both literals rewritten with `\\` continuations in 0.10.2; semantics unchanged"
}
]
},
{
"id": "BG-039",
"area": "background-tasks",
"behavior": "A `<task-notification>` whose `<summary>` opens `Background command \"...\"` is ALWAYS a background-command pulse whatever its quoted name says; the `Monitor` / `Scheduled` / `cron` classification comes from the summary PREFIX only.",
"depends": "csift's summary classifier routes on the leading phrase alone; the retired quoted-name heuristic (a `monitor` / `re-arm` / `liveness` word inside the quoted name) produced 40 spurious `monitor` records against zero genuine Monitor pulses on one project.",
"code": [
{
"path": "src/model/automation.rs",
"lines": "33-44",
"snippet": " /// Classify from the `<summary>`. Case-insensitive on the known leading prefixes; anything\n /// else (or a missing summary) is [`AutomationKind::Task`]. The `monitor`/`scheduled`/`cron`\n /// LEADING prefixes route a Monitor-tool pulse or termination notice\n /// (`Monitor event: …` / `Monitor \"…\" …`) to [`AutomationKind::Monitor`]. A `Background\n /// command \"…\"` pulse is ALWAYS `background-command`, whatever its quoted name says\n /// (v0.10.0: the former quoted-name heuristic - `monitor`/`re-arm`/`liveness` in the name\n /// routed to `Monitor` - predates the real Monitor tool and double-booked; measured on one\n /// project it produced 40 `monitor` records against zero genuine Monitor pulses). This\n /// does NOT cover `ScheduleWakeup` wakeup-tick prompts (isMeta records that never reach\n /// this classifier).\n #[must_use]\n pub fn from_summary(summary: Option<&str>) -> Self {"
}
],
"instrument": "`csift search 'task-notification' <project dir> --count-by label` and compare the `harness.notification.monitor` count with the number of `Monitor started (task ` arms in the same scope. Counting rule: one record per matched record on each side.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/model/automation.rs comment; CHANGELOG 0.10.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "In one project directory under ~/.claude/projects: csift search '' <project dir> -t harness.notification --count-by label ; csift search 'Background command \"[^\"]*(?i)(monitor|re-arm|liveness)' <project dir> -t harness.notification --count-by label ; csift search '' <project dir> -t harness.notification.monitor --no-truncate ; rg -o 'Monitor started \\(task [a-z0-9]+' over the same directory",
"observed": "Scope label census: 651 harness.notification.background-command, 96 workflow, 67 subagent, 1 monitor, 1 task (816 records). The quoted-name probe: 586 records whose summary is Background command \"<name containing monitor, re-arm or liveness>\" ALL classify harness.notification.background-command and ZERO classify .monitor. The single .monitor record in scope renders as '[monitor <task-id> <status>] Monitor event: \"<name>\"' - a summary-prefix match - and its arm is a genuine Monitor tool result, 'Monitor started (task <id>, persistent - runs until TaskStop or session end)' with toolUseResult {taskId, timeoutMs, persistent}, in the same session. 6 distinct Monitor arms exist in the scope (7 occurrences of the arm string).",
"rule": "One record per matched record on each side (--count-by label counts records, not regex occurrences); Monitor arms counted as distinct task ids in the 'Monitor started (task <id>' text.",
"note": "The retirement is confirmed by a counting rule a stranger can rerun: 586 notification records whose quoted background-command name contains monitor, re-arm or liveness - exactly the words the retired heuristic keyed on - now all land under harness.notification.background-command, and none under .monitor. The only .monitor record in the scope earns it by summary PREFIX (Monitor event:) and is backed by a real Monitor tool arm, so the prefix-only rule is doing the classifying. Note this scope is not the zero-genuine-Monitor project the code comment cites: 6 distinct Monitor arms exist here, of which only one produced a notification record in scope."
}
]
},
{
"id": "BASH-001",
"area": "bash-shell",
"behavior": "Claude Code keeps NO long-lived shell: every Bash tool call builds its own command string and spawns a fresh shell whose starting directory is a TRACKED cwd, and that tracked value is stamped on every jsonl record as the top-level `cwd` field - so `cwd` is not a per-session constant (measured: 19 of 7586 transcripts carry more than one value, the largest carrying 8; 18 records corpus-wide carry `/`). The field is present on subagent transcripts too (measured 454,020/454,020 user+assistant records across 7,502 subagent files) and is unchanged across compaction boundaries (231/231).",
"depends": "csift's bash operand resolution needs NO cross-command state: each record carries its own spawn cwd, so `files`/`recover` join a relative operand against the carrying record's own `cwd` at the zero-inference `cwd-joined` class, in a subagent lane exactly as in the main thread. A shared-shell model would attribute operands to the wrong directory, and an absent `cwd` would degrade every such row to `unresolved`.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "4-8",
"snippet": "//! ## How Claude Code manages the Bash tool's cwd (verified against CC 2.1.237)\n//!\n//! Claude Code does NOT keep a long-lived shell. Every Bash tool call spawns a fresh\n//! shell whose starting directory is a TRACKED cwd, and that tracked value is stamped\n//! on every jsonl record as the top-level `cwd` field. Concretely:"
},
{
"path": "src/bash_mutations/cwd.rs",
"lines": "31-32",
"snippet": "//! - `CwdJoined`: a relative operand before any `cd`; joined to the record's own `cwd`\n//! field. That value is data Claude Code wrote, so the join involves no inference."
}
],
"instrument": "python/jq over ~/.claude/projects: for every line whose `type` is user or assistant collect the top-level `cwd` and count distinct values per transcript; separately count such records lacking the field under `*/subagents/**/*.jsonl` (expect zero). Counting rule: one `cwd` value per record, deduplicated per file.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; CHANGELOG 0.8.0; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import os,json,collections;R=os.path.expanduser('~/.claude/projects');per=collections.Counter();ua=0;miss=0;sub=0;submiss=0;slash=0;\\nfor dp,dn,fns in os.walk(R):\\n for fn in fns:\\n if not fn.endswith('.jsonl') or fn=='journal.jsonl': continue\\n p=os.path.join(dp,fn);isub='subagents' in p.split(os.sep);seen=set()\\n for raw in open(p,'rb'):\\n try: r=json.loads(raw)\\n except Exception: continue\\n if r.get('type') not in ('user','assistant'): continue\\n c=r.get('cwd')\\n ...\\\" (full script: walk every *.jsonl under ~/.claude/projects, json-parse every line, and for each type user|assistant record read the top-level cwd; per file count distinct cwd values; count records missing cwd, split top-level vs subagent lane; count records whose cwd is exactly '/'; at every system/compact_boundary compare the cwd of the last user|assistant record before it with the first after it)",
"observed": "7586 transcripts. Top-level lane: 201151 user|assistant records, 15 with no cwd. Subagent lane: 454020 user|assistant records, 0 with no cwd. Distinct-cwd-per-file histogram: 7563 files with 1 value, 11 with 2, 6 with 3, 1 with 4, 1 with 8 (max 8, not 7); 10 top-level and 9 subagent files carry more than one value. 18 records carry cwd \"/\". 232 compact_boundary records; the cwd before and after the boundary is identical in 231/231 comparable boundaries, 0 differ. Binary corroboration: one command string is built per call and spawned with its own cwd - strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'buildExecCommand' shows 'let{commandString:ht,cwdFilePath:Lt}=await Ue.buildExecCommand(e,{id:ot,...})' with a per-call id.",
"rule": "One cwd value per type user|assistant record, deduplicated per file. A file counts as multi-cwd when its distinct set exceeds one. Boundary comparison: one comparison per compact_boundary that has a timestamped user|assistant record on each side.",
"note": "Structure holds; the illustrative numbers were stale. Two shifts worth recording: (a) multi-cwd transcripts are rare (19 of 7586) but the maximum is now 8 distinct values in one file, not 7; (b) the `/` cwd is now 18 records corpus-wide rather than 51 in one session, so the earlier session has aged out. Subagent coverage strengthened by two orders of magnitude and is still exactly 100%. All cited code lines (src/bash_mutations/cwd.rs 4-8 and 31-32) exist verbatim in the current file."
}
]
},
{
"id": "BASH-002",
"area": "bash-shell",
"behavior": "Claude Code recovers a Bash call's ending directory by appending `pwd -P >| <tmp>/claude-<id>-cwd` as the last element of an `&&`-joined command chain and reading that file back; because the read-back is `&&`-chained, a command that exits non-zero never advances the tracked cwd (the file is never written, the read throws, the tracker is left alone). The read-back result is consumed only under `if(result && !pinned && !result.backgroundTaskId)`, so a backgrounded command is excluded by an explicit `backgroundTaskId` gate and a PINNED lane (any subagent lane) is excluded outright. Under a sandbox the file is named `cwd-<id>` in the sandbox temp dir instead.",
"depends": "csift can trust the record `cwd` as the shell's true spawn directory without replaying exit status; the lexical `cd` inference is applied only INSIDE one command and is labelled `cd-tracked`, so a `cd` that failed at runtime never silently relocates later operand resolution.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "10-12",
"snippet": "//! - The runner appends `&& pwd -P >| <tmp>/claude-<id>-cwd` to the command and reads\n//! the file back afterward. Because the read-back is `&&`-chained, a command that\n//! exits non-zero, and any backgrounded command, never advances the tracked cwd."
}
],
"instrument": "`strings` over the installed Claude Code binary for 2.1.258, grepping for `pwd -P` and the `claude-`...`-cwd` tempfile template: expect one read-back site, `&&`-chained. Live check: run `cd /tmp && false` in one Bash call, then `csift show @main --turn -1 --raw | jq -r .cwd` on the next record - the value must be unchanged. Counting rule: one tracked value per completed call.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'pwd -P' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{60}claude-.{0,40}-cwd.{60}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'if\\(Do&&!C&&!Do\\.backgroundTaskId\\).{0,300}' ; then live, in two separate Bash calls: (1) `cd /private/tmp && false` (2) `pwd -P`",
"observed": "Exactly one read-back site: `ke.push(`pwd -P >| ${Ko([W])}`)` immediately followed by `let De=ke.join(\" && \")`, so the read-back is the last element of an &&-joined chain; an optional `CLAUDE_CODE_SHELL_PREFIX` wraps the finished chain. The target file is `W=U?u$e(j,`cwd-${v.id}`):u$e(B,`claude-${v.id}-cwd`)` - `claude-<id>-cwd` in the normal temp dir, `cwd-<id>` under a sandbox temp dir. The read-back is consumed under a triple gate: `if(Do&&!C&&!Do.backgroundTaskId)` then `readFileBytes(Dr)` ... `du(wo,d,Ot)`; on any throw it records `tengu_shell_set_cwd {success:false}` and leaves the tracked cwd alone. LIVE: call 1 `cd /private/tmp && false` exited 1; call 2 `pwd -P` printed the unchanged project directory.",
"rule": "String presence and adjacency in the 2.1.258 binary (one occurrence per site); then one tracked value per completed call, read by running `pwd -P` as the whole of the next call.",
"note": "Both stated suppressors verified. The correction adds a THIRD suppressor the claim did not know about: the same read-back is skipped when the lane is pinned, which is the case for every subagent lane (see BASH-003). Also worth recording for csift: the tempfile name has a second form under sandboxing. src/bash_mutations/cwd.rs 10-12 exists verbatim."
}
]
},
{
"id": "BASH-003",
"area": "bash-shell",
"behavior": "In a TOP-LEVEL session a `cd` into a project SUBDIRECTORY persists silently into later Bash calls - the tracked cwd, and so the record's `cwd` field, follows it with no notice of any kind (measured 45/49 such commands, still true at 2.1.251/2.1.252/2.1.257). In a SUBAGENT lane the cwd is PINNED: the post-command read-back is skipped, so the same `cd` does not persist (measured 1/2265) and the record `cwd` stays the session directory for the whole lane.",
"depends": "csift never assumes the record `cwd` equals the project root: `list` deliberately reports a session's FIRST-seen `cwd` (last-seen could be a transient subdirectory) and `files` never treats a drifted cwd as a different project.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "13-14",
"snippet": "//! - A `cd` into a SUBDIRECTORY of the project persists silently into later tool\n//! calls; the tracked cwd (and so the record `cwd` field) follows it. A reset back"
}
],
"instrument": "`csift search 'cd ' <target> -t agent.tool.use --format json | jq -r .cwd` and compare consecutive tool_use records across a subdirectory `cd`: the value follows it, with no reset notice on the intervening result. Counting rule: one comparison per adjacent (use, next-use) pair in one transcript.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"walk ~/.claude/projects; per transcript take the FIRST cwd seen on a user|assistant record; for every Bash tool_use whose command matches ^\\\\s*cd\\\\s+(<absolute path under that first cwd>) remember the target; count it a PERSIST when a LATER record in the same transcript carries that target as its own top-level cwd; bucket by lane and by record version\\\" ; plus live, in two separate Bash calls: (1) `cd <project>/src/bash_mutations && pwd -P` (2) `pwd -P` ; plus csift show @trap:<marker> --line 1.. --raw --max-count 0 | (count distinct cwd)",
"observed": "Top-level lane: 49 `cd <absolute subdirectory>` commands, 45 followed by a later record carrying that subdirectory as its cwd (91.8%), including at versions 2.1.257 (2), 2.1.252 (1), 2.1.251 (4). Subagent lane: 2265 such commands, 1 persisted (0.04%). LIVE in this subagent lane: call 1 `cd .../src/bash_mutations && pwd -P` printed the subdirectory; call 2 `pwd -P` printed the project root again, with no notice of any kind. csift raw read of this lane: 199 user|assistant records, exactly 1 distinct cwd. Binary: the cwd read-back and the reset check are both wrapped in a pin test - `Ne=!Ie||Pe?.pinCwd===!0` with `Ie=!n.agentId`, and the reset call site is `if(W=n.session.project.cwd,!Ne){...}`.",
"rule": "One comparison per `cd <absolute subdirectory>` Bash tool_use: persist = some later record in the same transcript carries that exact path as its top-level cwd. Bucketed by lane (a path component named subagents) and by the record's version field.",
"note": "The claim as written is lane-blind and is false for subagent lanes on current Claude Code. This matters for csift beyond documentation: `cd-tracked` operand resolution inside one subagent command is unaffected (it is in-command lexical tracking), but any reasoning that a subagent's later record cwd reflects an earlier `cd` is wrong. It also means the reset notice of BASH-004 can never appear in a subagent transcript, which the corpus confirms (0 of 5254). src/bash_mutations/cwd.rs 13-14 exists verbatim."
}
]
},
{
"id": "BASH-004",
"area": "bash-shell",
"behavior": "Claude Code resets the shell cwd when the shell ENDED outside the original cwd unioned with the `/add-dir` directories (membership is an every-path-inside-a-working-directory test), and the reset prints `Shell cwd was reset to <path>` into the tool result. The reset check runs only in UN-PINNED lanes: in a subagent lane it is skipped entirely, so the notice never appears there (0 of 5254 corpus notices sit in a subagent transcript).",
"depends": "The notice is the only visible trace that a reset happened; csift treats it as a positive marker only and never parses it to learn the directory - it reads the record's own `cwd` field.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "13-18",
"snippet": "//! - A `cd` into a SUBDIRECTORY of the project persists silently into later tool\n//! calls; the tracked cwd (and so the record `cwd` field) follows it. A reset back\n//! to the original directory happens only when the shell ends OUTSIDE the original\n//! cwd and the `/add-dir` set, and it prints \"Shell cwd was reset to <path>\" into\n//! the tool result. With `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` set truthy, the\n//! reset happens on every call and silently."
}
],
"instrument": "`csift search 'Shell cwd was reset to' --count-by session` over ~/.claude/projects. Counting rule: one record per reset notice.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search 'Shell cwd was reset to' --count-by version ; csift search 'Shell cwd was reset to' --count-by session ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,300}Shell cwd was reset to.{0,300}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function e_\\(e,n,r,o=fb\\(n\\)\\).{0,160}'",
"observed": "csift: 5356 matched records across 61 session keys; by version 46 at 2.1.258, 22 at 2.1.257, 15 at 2.1.252, 37 at 2.1.251. Binary, verbatim: `var gBt=(e)=>`${e.trim()}\\nShell cwd was reset to ${ne()}``, `ymt=/(?:^|\\n)(Shell cwd was reset to .+)$/`, and `function hBt(e,n){let r=e.project.cwd,o=ye(),d=Rpr();if(d||r!==o&&!e_(r,n)){try{du(o,e)}catch{return!0}if(!d)return s(\"tengu_bash_tool_reset_to_original_dir\",{}),!0}return!1}`. The membership test is `function e_(e,n,r,o=fb(n)){let d=r??Ir(e),f=Array.from(o).flatMap((_)=>QGt(_));return d.every((_)=>f.some((w)=>Dp(_,w,{caseFold:!1,uncShapeParity:!0})))}` - every path must sit inside one of the working directories. The call site is gated on the pin flag: `if(W=n.session.project.cwd,!Ne){let Rr=I();if(hBt(n.session,Rr.toolPermissionContext))U=gBt(\"\")}`.",
"rule": "One record per line carrying the notice (csift census, one record per matched record); binary evidence is string presence plus the surrounding branch, one occurrence per site.",
"note": "Still live: 46 notice records at 2.1.258 out of 5356 corpus-wide. Two additions: (a) the reset also fires unconditionally when CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR is truthy, in which case it is silent - the `if(!d)` arm is what emits the notice and the telemetry; (b) the notice builder trims whatever stderr preceded it and joins with a single newline, which is what produces the two rendered forms measured in BASH-006. src/bash_mutations/cwd.rs 13-18 exists verbatim."
}
]
},
{
"id": "BASH-005",
"area": "bash-shell",
"behavior": "With the environment variable `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` truthy (`1`, `true`, `yes`, `on`, lowercased and trimmed) Claude Code resets the shell cwd on EVERY Bash call and emits NO notice; the variable sits in the settings.json `env` allowlist, so it is settable per project.",
"depends": "csift resolves operands per record instead of predicting an ending cwd across calls, so a session run under this setting still resolves correctly; a tracker that carried a predicted cwd forward would be silently wrong for every such session, with no notice available to detect it.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "17-18",
"snippet": "//! the tool result. With `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` set truthy, the\n//! reset happens on every call and silently."
}
],
"instrument": "`strings` over the installed Claude Code binary for 2.1.258 grepping `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR`: expect one settings-env allowlist entry plus one truthiness-gated reset branch. Counting rule: one string occurrence per site.",
"located": {
"claude_code": "2.1.237",
"csift": null,
"source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,260}CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR.{0,260}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Le\\(t\\)\\{if\\(!t\\).{0,180}'",
"observed": "5 occurrences. (1) the reader: `function Rpr(){return Le(process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR)}`. (2) the truthiness helper, verbatim: `function Le(t){if(!t)return!1;if(typeof t===\"boolean\")return t;let e=String(t).toLowerCase().trim();return[\"1\",\"true\",\"yes\",\"on\"].includes(e)}`. (3) an exports map entry `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR:()=>WL`. (4) an alphabetically sorted settings env allowlist array between \"BASH_MAX_TIMEOUT_MS\" and \"CLAUDE_CODE_API_KEY_HELPER_TTL_MS\". (5) a second grouped env array. The gated reset branch is `if(d||r!==o&&!e_(r,n)){try{du(o,e)}catch{return!0}if(!d)return s(\"tengu_bash_tool_reset_to_original_dir\",{}),!0}` with `d=Rpr()` - when d is truthy the reset runs on every call and the function falls through to `return!1`, so no notice and no telemetry are emitted.",
"rule": "One string occurrence per site; truthiness set read verbatim from the helper; the silence is read off the `if(!d)` guard that gates the only notice-producing return.",
"note": "Every element of the claim - the exact truthy set (1/true/yes/on, lowercased and trimmed), the every-call reset, the silence, and the settings env allowlist membership - is confirmed verbatim in the 2.1.258 binary. Not exercised end-to-end on disk: no corpus session ran with the variable set (it is unset in this environment), and it cannot be injected from inside a Bash call because Claude Code reads it from its own process env. One lane caveat, not a correction to the claim: the reset call site is itself behind the pin flag, so in a subagent lane the branch is never reached (the cwd never advances there anyway). src/bash_mutations/cwd.rs 17-18 exists verbatim."
}
]
},
{
"id": "BASH-006",
"area": "bash-shell",
"behavior": "The cwd-reset notice has exactly two rendered forms - it either starts the tool_result content (5 records, when stdout was empty) or is preceded by a single newline (5249) - and it lands in BOTH the tool_result content and `toolUseResult.stderr`: measured over 5,254 carrying records, 5,254 carried it in both places, 0 content-only and 0 stderr-only. In `toolUseResult.stderr` the stored form is invariably one leading newline then the sentence (5,254/5,254). The path inside the notice equals the carrying record's own `cwd` in 5,254/5,254 cases. It is the last line of the rendered content in 5,122/5,254 - the remainder are followed by the `staleReadFileStateHint` sentence or a background block.",
"depends": "csift reads the `cwd` field rather than parsing the notice, so a wording change costs nothing while a change to the `cwd` field would break every bash operand resolution.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "15-18",
"snippet": "//! to the original directory happens only when the shell ends OUTSIDE the original\n//! cwd and the `/add-dir` set, and it prints \"Shell cwd was reset to <path>\" into\n//! the tool result. With `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` set truthy, the\n//! reset happens on every call and silently."
}
],
"instrument": "`rg -c 'Shell cwd was reset to' ~/.claude/projects --glob '*.jsonl'`, then for each hit compare the notice's path to the carrying record's `cwd` and check whether the same text also appears in `toolUseResult.stderr`. Counting rule: one record per matching line; equality counted per record carrying the notice.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"walk ~/.claude/projects; keep only lines containing the byte string 'Shell cwd was reset to'; json-parse; a record COUNTS only when re.compile(r'(?:^|\\\\n)Shell cwd was reset to .+$', re.M) matches inside toolUseResult.stderr; then check the same regex against the concatenated tool_result block text, classify the character run before the match, and compare the path in the notice with the record's own top-level cwd\\\"",
"observed": "5254 records carry a genuine notice in toolUseResult.stderr. In 5254/5254 the stderr form is exactly one leading newline then the sentence, with nothing before it. In 5254/5254 the notice also appears in the rendered tool_result content: 5249 preceded by a newline, 5 starting the content (that is exactly two textual forms). stderr-only 0. The path inside the notice equals the carrying record's own cwd in 5254/5254. The notice is the LAST line of the rendered content in 5122/5254; the other 132 are followed by a later section, dominated by the staleReadFileStateHint sentence '[This command modified N file(s) you have previously read: ...]' with a few background blocks. All 5254 are top-level-lane records; versions include 10 at 2.1.258, 22 at 2.1.257, 15 at 2.1.252, 37 at 2.1.251. A looser filter that accepted the byte string anywhere added 8 'content-only' records, all of them tool output or prose quoting the sentence rather than notices.",
"rule": "One record per notice, admitted only when the sentence starts a line inside toolUseResult.stderr; both-places, path-equality and last-line are each counted per admitted record.",
"note": "The claim's shape survives at a larger n; three numbers and one wording need fixing. (a) The single 'content-only' record was a false positive of a byte-level filter: with the sentence required to start a line inside toolUseResult.stderr, both-places is 100%. (b) Path equality is now 5254/5254 rather than a 4229-record subset. (c) 'as its last line' is only true 97.5% of the time - the render order is stdout, stderr, background block, staleReadFileStateHint, rate-limit hint, joined by newlines, so anything after the stderr section pushes the notice off the end. Do not key a parser on last-line position. src/bash_mutations/cwd.rs 15-18 exists verbatim."
}
]
},
{
"id": "BASH-007",
"area": "bash-shell",
"behavior": "Claude Code's post-command cwd update lands between the tool_use record and its result record, so the TOOL_USE record carries the spawn (pre-command) directory while the RESULT record can already carry the post-command value: measured 52 of 119,070 Bash use/result pairs (0.0437%), with the neighbouring records agreeing with the tool_use side in 52/52 and with the result side in 46/52.",
"depends": "csift's ANCHOR LAW reads `cwd` off the tool_use record everywhere (`files`, `recover`, stale-hint path resolution); anchoring on the result record instead silently mis-joins roughly one operand in four hundred, with no error.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "19-21",
"snippet": "//! - ANCHOR LAW: read the `cwd` off the TOOL_USE record, not the result record. The\n//! post-command cwd update is asynchronous and in about 0.24% of calls the result\n//! record still carries the pre-command value."
}
],
"instrument": "Pair each Bash `tool_use` block id with its `tool_result` carrier in one transcript and compare the two records' `cwd` fields. Counting rule: disagreeing pairs divided by all Bash use/result pairs in the file (expect about 0.24%).",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs ANCHOR LAW; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"walk ~/.claude/projects; per transcript map every Bash tool_use block id to its carrying record's top-level cwd; when a tool_result block quotes that id, compare the two records' cwd; for each disagreement also read the nearest preceding and nearest following user|assistant record's cwd\\\"",
"observed": "119070 Bash use/result pairs; 52 disagree (0.0437%), 50 in the top-level lane and 2 in subagent lanes; versions include 2.1.257 (1), 2.1.252 (1), 2.1.251 (11), 2.1.208 (18). Direction, in all 52: the nearest PRECEDING record agrees with the TOOL_USE record (52/52); the nearest FOLLOWING record agrees with the RESULT record in 46/52 and with the tool_use in the remaining 6. So the tool_use record carries the pre-command spawn directory and the result record has already picked up the post-command value.",
"rule": "Disagreeing pairs divided by all Bash tool_use/tool_result pairs joined by tool_use_id, corpus-wide, one comparison per pair; direction decided per disagreement by which side the neighbouring records agree with.",
"note": "The ANCHOR LAW is confirmed correct and still load-bearing, but the rationale printed beside it in src/bash_mutations/cwd.rs 19-21 has the polarity inverted: it is not that the result record lags behind, it is that the result record RUNS AHEAD. The consequence is the same and if anything sharper - anchoring on the result record would join a relative operand against the directory the command left the shell in, i.e. after its own `cd`, which is exactly the wrong directory. The rate is 5.5x lower than the recorded 0.24%: use 0.0437% (about 1 in 2,300). The cited lines exist verbatim; only the wording of the second sentence needs replacing."
}
]
},
{
"id": "BASH-008",
"area": "bash-shell",
"behavior": "A backgrounded Bash command in which ANY top-level segment's first token is `cd`/`pushd`/`popd`/`chdir` gets the notice `Session cwd remains <path>; directory changes made by the backgrounded command do not apply to subsequent commands.`, carried on `toolUseResult.backgroundCwdHint` and appended to the background block of the result content (measured 1,204 records; 1,204/1,204 match the sentence exactly, appear in the rendered content, and name the carrying record's own cwd).",
"depends": "csift treats a backgrounded command as never advancing the tracked cwd, matching the harness; no csift surface parses the hint, so a rename is not load-bearing, but the underlying suppression is.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "10-12",
"snippet": "//! - The runner appends `&& pwd -P >| <tmp>/claude-<id>-cwd` to the command and reads\n//! the file back afterward. Because the read-back is `&&`-chained, a command that\n//! exits non-zero, and any backgrounded command, never advances the tracked cwd."
}
],
"instrument": "`rg -c 'backgroundCwdHint' ~/.claude/projects --glob '*.jsonl'`; expect a four-digit count in a large corpus. Counting rule: one occurrence per Bash result object carrying the key.",
"located": {
"claude_code": "2.1.237",
"csift": null,
"source": "dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"walk ~/.claude/projects; keep lines containing 'backgroundCwdHint'; json-parse; require toolUseResult to be an object carrying the key; match the value against ^Session cwd remains (.+); directory changes made by the backgrounded command do not apply to subsequent commands\\\\.$; compare group 1 with the record's own cwd; check whether the same string appears in the rendered tool_result content\\\" ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,700}backgroundCwdHint.{0,300}'",
"observed": "1204 records carry toolUseResult.backgroundCwdHint. 1204/1204 match the sentence exactly, 1204/1204 also appear inside the rendered tool_result content, and the path in the hint equals the carrying record's own cwd in 1204/1204. Versions include 3 at 2.1.258 and 3 at 2.1.251. Binary: the value is built as `hn=ke.backgroundTaskId&&S9(e.command)?`Session cwd remains ${ne()}; directory changes made by the backgrounded command do not apply to subsequent commands.`:void 0`, appended to the background block (`if(I)_e+=\"\\n\"+I`) which is then joined into `[fe,me,_e,j,W].filter(Boolean).join(\"\\n\")`. The trigger predicate is `function S9(e){return ep(e).some((n)=>_O(n.trim()))}` over `function _O(e){let n=Uh(uC(e))[0];return n===\"cd\"||n===\"pushd\"||n===\"popd\"||n===\"chdir\"}`. The tool schema describes the field as \"Model-facing note that the session cwd was not changed by a backgrounded command containing a directory-change builtin (cd/pushd/popd/chdir)\".",
"rule": "One occurrence per Bash result object carrying the key; sentence match, content presence and path equality each counted per carrying record.",
"note": "Only the trigger wording needed fixing: it is not 'whose text starts with' a directory-change builtin - the command is split into top-level segments and ANY segment whose first token is cd/pushd/popd/chdir arms the hint, so `make build && cd /tmp` in a backgrounded command is enough. The four-digit corpus count the claim predicted is exactly right (1,204). src/bash_mutations/cwd.rs 10-12 exists verbatim."
}
]
},
{
"id": "BASH-009",
"area": "bash-shell",
"behavior": "A Bash result's `toolUseResult` object carries the always-present key set `{stdout, stderr, interrupted, isImage, noOutputExpected}` (44,340/44,340 objects) plus at least eleven observed optional keys - `backgroundTaskId`, `backgroundCwdHint`, `staleReadFileStateHint`, `gitOperation`, `persistedOutputPath`, `persistedOutputSize`, `returnCodeInterpretation`, `dangerouslyDisableSandbox`, `timedOutAfterMs`, `assistantAutoBackgrounded`, `backgroundEndsWithFinalResponse` (2.1.258 can also emit `backgroundedByUser`, `backgroundedByTurnAbort`, `backgroundedToDeliverMessage`, `ghRateLimitHint`) - and no structured field naming the paths the command touched: the only path-valued key, `persistedOutputPath`, names the externalized stdout file, and modified-file names appear only inside the prose of `staleReadFileStateHint`. The object is absent entirely on 73,006 subagent-lane results and is a bare string on 1,792 error-path results. The rendered result content is assembled as stdout, stderr, the background block, `staleReadFileStateHint` and the rate-limit hint, joined by newlines.",
"depends": "csift keeps `toolUseResult` UNPARSED as a `RawValue` and reads only small named fields through `Record::tur_probe`, so a renamed key silently removes a `recover` freshness boundary or a `search --resolve-persisted` pointer with no error. Because the object names no path, bash file attribution is a lexical parse of `input.command` and is flagged `(heuristic)` on every surface - text, JSON, help and SKILL.",
"code": [
{
"path": "src/model/record.rs",
"lines": "123-124",
"snippet": " #[serde(default, rename = \"toolUseResult\")]\n pub tool_use_result: Option<Box<serde_json::value::RawValue>>,"
},
{
"path": "src/model/record.rs",
"lines": "306-308",
"snippet": " #[serde(rename = \"persistedOutputPath\")]\n pub(crate) persisted_output_path: Option<serde_json::Value>,\n pub(crate) status: Option<serde_json::Value>,"
}
],
"instrument": "python over ~/.claude/projects: for every Bash `tool_result` carrier, union the top-level keys of `toolUseResult`; expect the five always-present keys plus the six optional ones, and no path key. Counting rule: one key set per Bash result record, unioned corpus-wide.",
"located": {
"claude_code": "2.1.237",
"csift": "0.2.0",
"source": "SPEC.md section 6.6; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"walk ~/.claude/projects; per transcript collect Bash tool_use block ids; for every tool_result quoting one, record the python type of toolUseResult, union its top-level keys, count records missing any of stdout/stderr/interrupted/isImage/noOutputExpected, and where both a non-empty stderr and a staleReadFileStateHint are present check which appears first in the rendered block text\\\" ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'mapToolResultToToolResultBlockParam\\(\\{interrupted.{0,2400}'",
"observed": "119138 Bash results seen: 44340 carry an object toolUseResult, 1792 carry a bare string, 73006 carry none (the subagent lane). Of the 44340 objects, 0 are missing any of the five core keys. Key census: stdout/stderr/interrupted/isImage/noOutputExpected 44340 each; then backgroundTaskId 2980, backgroundCwdHint 1204, staleReadFileStateHint 1077, gitOperation 827, persistedOutputPath 400, persistedOutputSize 400, returnCodeInterpretation 249, dangerouslyDisableSandbox 102, timedOutAfterMs 59, assistantAutoBackgrounded 21, backgroundEndsWithFinalResponse 8 - eleven optional keys, not six. 2.1.258 additionally constructs backgroundedByUser, backgroundedByTurnAbort, backgroundedToDeliverMessage and ghRateLimitHint on the same object. Render assembly, verbatim from the binary: `return{tool_use_id:z,type:\"tool_result\",content:[fe,me,_e,j,W].filter(Boolean).join(\"\\n\"),is_error:e}` where fe is stdout (or the persisted-output pointer), me is stderr, _e is the background block, j is staleReadFileStateHint and W is ghRateLimitHint. On disk stderr precedes staleReadFileStateHint in 134/134 records carrying both.",
"rule": "One key set per Bash tool_result record whose toolUseResult is an object, unioned corpus-wide; order checked once per record carrying both a non-empty stderr and a stale-read hint.",
"note": "The always-present five and the render assembly are exactly right (the latter now confirmed verbatim in the binary, with the rate-limit hint identified as ghRateLimitHint). Three corrections: the optional set is eleven observed rather than six - notably backgroundCwdHint was already ledgered as BASH-008 but missing from this key list; toolUseResult is not always an object (bare string on 1,792 error-path results, absent on the whole subagent lane); and 'NO path field of any kind' overstates it - persistedOutputPath is a path, just not an operand path. csift's design consequence is unchanged and if anything reinforced: no key names the mutated files, so bash file attribution stays a lexical parse of input.command. src/model/record.rs 116-117 and 299-301 exist verbatim."
}
]
},
{
"id": "BASH-010",
"area": "bash-shell",
"behavior": "`toolUseResult` separates `stdout` from `stderr`, and when stderr is empty and no other rendered section is present the tool_result block text equals `stdout` byte-exactly - no wrapper decoration and no line-number gutter (measured 34,246/34,246 such results corpus-wide, 0 exceptions). When stdout is empty the block instead carries the synthetic text `(Bash completed with no output)`. The renderer does apply a leading-blank-line strip and a trailing trim to stdout, which is a no-op on every stored value measured. `cat -n` and `nl` are not absent from real sessions (563 and 96 of 119,190 Bash commands respectively), so a read anchor must gate on the command shape rather than assume no gutter.",
"depends": "csift's `recover` read anchors take a gated command's stdout as the file window verbatim, so a wrapper prefix or a gutter would splice decoration into a reconstructed file; the anchor therefore also demands a non-error, non-interrupted result.",
"code": [
{
"path": "src/recover/bash_anchors.rs",
"lines": "5-11",
"snippet": "//! The lexical classifier decides WHAT a command shape means; this layer supplies\n//! the transcript-side gates:\n//! - the command's RESULT must not be an error (`failed_ids` - a failed write never\n//! landed, a failed read proves nothing);\n//! - a READ anchor additionally needs the top-level `toolUseResult` echo with EMPTY\n//! stderr, `interrupted` false, and NO persisted-output pointer (an externalized\n//! stdout is not the inline text). The echo is a per-LANE fact, not a per-level one"
}
],
"instrument": "python over one transcript: for Bash results with empty `toolUseResult.stderr`, compare `toolUseResult.stdout` to the tool_result block's rendered text. Counting rule: one byte comparison per Bash result with empty stderr.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "SPEC.md section 6.7; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"walk ~/.claude/projects; for every Bash tool_result whose toolUseResult is an object with stderr=='' and a non-empty stdout and no persistedOutputPath, no backgroundTaskId, interrupted falsey, no staleReadFileStateHint and no returnCodeInterpretation, compare toolUseResult.stdout byte-for-byte with the concatenated text of the tool_result block, and also against stdout with leading blank lines stripped and trailing whitespace trimmed\\\" ; a second pass counting Bash tool_use commands containing 'cat -n' or a top-level segment whose first token is 'nl'",
"observed": "34246 comparisons: 34246 byte-exact, 0 needing the leading-blank-line strip plus trimEnd, 0 differing. The binary does normalize on the way out - `let fe=n;if(n)fe=n.replace(/^(\\s*\\n)+/,\"\"),fe=fe.trimEnd()` - so the equality is telling us the stored stdout is already in that normal form. Separately, when stdout is empty the rendered text is the synthetic string `(Bash completed with no output)` rather than an empty block (348 such records). Line-numbering commands are NOT absent from the corpus: of 119,190 Bash commands, 563 contain `cat -n` and 96 have `nl` as a segment head.",
"rule": "One byte comparison per Bash result with an object toolUseResult, empty stderr, non-empty stdout and none of the other rendered sections present; the command census counts one command per Bash tool_use block.",
"note": "The load-bearing half is confirmed at 1,600x the original sample size: nothing is spliced into the block, so recover can take a gated command's stdout as the file window verbatim. Two additions the anchor layer should know: (a) the empty-stdout case renders a synthetic sentence, so an anchor that reads the BLOCK text rather than toolUseResult.stdout would splice `(Bash completed with no output)` into a reconstructed empty file - reading the echo, as src/recover/bash_anchors.rs already requires, avoids this; (b) the parenthetical about `cat -n`/`nl` describes the 1,085-command sample only and does not generalise - both appear in the wider corpus, which is a reason the shape gate (plain `cat`/`head -n N`/`sed -n 'A,Bp'`) is doing real work. src/recover/bash_anchors.rs 5-13 exists verbatim."
}
]
},
{
"id": "BASH-011",
"area": "bash-shell",
"behavior": "`toolUseResult.returnCodeInterpretation` is not a generic non-zero marker. Claude Code emits it only for a CLOSED command-to-interpretation table (grep/rg/egrep/fgrep -> 'No matches found', find -> 'Some directories were inaccessible', diff -> 'Files differ', test and [ -> 'Condition is false', plus `git grep`/`git diff` routed to the same two), and only at exit code exactly 1, which that table classifies as NON-error (isError is n>=2). Every other non-zero exit falls to the default interpreter, which marks the call an error and writes no such field. The half that survives is the second: absence does not imply exit zero -- 91 of 91 Bash results flagged is_error:true carried no field. Measured share 30/4599 = 0.65% of Bash results in scope.",
"depends": "csift never infers exit status from the absence of that key; `recover`'s content-anchor gates demand a positively clean result (non-error AND empty stderr) rather than trusting a missing field.",
"code": [
{
"path": "src/bash_mutations/anchors.rs",
"lines": "18-22",
"snippet": "//! whole command to a CLEAN result (exit ok AND empty stderr: a failing\n//! cat/tee/echo always writes stderr, so a clean echo proves the write landed even\n//! in a `;`/newline chain whose exit code only reflects the LAST command). The\n//! anchor's own segment must also be free of substitutions/subshells, and no OTHER\n//! part of the command may touch the same resolved path (`same_path_hits`)."
}
],
"instrument": "`rg -c 'returnCodeInterpretation' ~/.claude/projects --glob '*.jsonl'` against the count of Bash result records. Counting rule: one occurrence per Bash `toolUseResult` object.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 $B | rg -o '.{0,500}Files differ.{0,200}' AND strings -n 6 $B | rg -o '.{0,400}Semantic interpretation for non-error exit codes.{0,60}' AND an inline python3 census over $P/*.jsonl pairing every Bash tool_use id to its tool_result carrier and reading toolUseResult",
"observed": "Binary: the field's own schema text is \"Semantic interpretation for non-error exit codes with special meaning\". The producer is a closed table -- x$=(e)=>(n,r,o)=>({isError:n>=2,message:n===1?e:void 0}) and Wzo=new Map([[\"grep\",x$(\"No matches found\")],[\"rg\",x$(\"No matches found\")],[\"egrep\",x$(\"No matches found\")],[\"fgrep\",x$(\"No matches found\")],[\"find\",x$(\"Some directories were inaccessible\")],[\"diff\",x$(\"Files differ\")],[\"test\",x$(\"Condition is false\")],[\"[\",x$(\"Condition is false\")]]) -- with a default interpreter jzo=(e,n,r)=>({isError:e!==0,message:e!==0?`Command failed with exit code ${e}`:void 0}) and a git arm returning the grep/diff interpreter for `git grep`/`git diff`. Corpus: 4599 Bash tool_result carriers, 30 carry returnCodeInterpretation (0.65%); the ONLY two distinct values are 'No matches found' (29) and 'Files differ' (1). 123 carriers have non-empty toolUseResult.stderr and 91 carry is_error:true -- 123/123 and 91/91 of those carry NO returnCodeInterpretation.",
"rule": "One key set per Bash tool_result record (a tool_result block whose tool_use_id resolves to a tool_use block named Bash), counted over every top-level transcript in $P. Ratio = records carrying the key over all Bash tool_result records. The absence half is counted as: Bash results with is_error:true that carry the key (expect 0).",
"note": "Not Claude Code drift: the same command-to-interpretation table is present in 2.1.229, 2.1.234, 2.1.241 and 2.1.252 as well as 2.1.258 (one match each for the `[\"grep\",...(\"No matches found\")]` entry), so the ledger wording was wrong when written rather than overtaken. The csift consequence in the depends clause is unaffected and in fact strengthened: since the field is emitted for a narrow non-error subset only, inferring exit status from its absence would be wrong in both directions, and recover's positively-clean gate (non-error AND empty stderr) is the right instrument. Code site confirmed verbatim at src/bash_mutations/anchors.rs lines 18-22."
}
]
},
{
"id": "BASH-012",
"area": "bash-shell",
"behavior": "The omission belongs to the WORKFLOW subagent lane specifically, not to subagent transcripts as a class. A Bash tool_result under <session>/subagents/workflows/wf_*/agent-*.jsonl carries no top-level toolUseResult echo at all (0 of 1302 at CC 2.1.258; 0 of 3867 across all versions in scope). A Bash tool_result in the two BUILT-IN-location lanes -- <session>/subagents/agent-*.jsonl, covering both one-shot Task/Agent subagents and named teammates -- does carry the echo, at 283 of 286 (99.0%) at CC 2.1.258. So stdout/stderr/interrupted exist in built-in subagent lanes as well as top-level ones, and are missing only in the workflow lane.",
"depends": "`recover`'s READ content anchors are gated on the ECHO ITSELF, not on the lane: `gated_stdout` admits a carrier only when its top-level `toolUseResult` carries a `stdout` string with empty stderr, `interrupted` false and no `persistedOutputPath`; no code on the anchor path inspects the transcript's lane. So read anchors reach every lane that carries the echo - the main lane and both built-in-location subagent shapes (one-shot Task/Agent and teammate) - and are unreachable only in the WORKFLOW lane, which carries none. WRITE anchors, being input-side, work in every lane.",
"code": [
{
"path": "src/recover/bash_anchors.rs",
"lines": "279-286",
"snippet": "/// The COMPLETENESS-gated stdout of a Bash result: the FIRST carrier record in this\n/// turn answering the tool_use id, whose `toolUseResult` echoes the command output.\n/// `None` when that carrier lacks the echo (workflow lanes never carry it; built-in\n/// and teammate lanes and the main lane do - the module doc has the measurement),\n/// stderr is non-empty, the command was interrupted, or the output was externalized\n/// to a persisted file (the inline text is not the whole stdout then). The gate is\n/// data-driven: no lane predicate, and no second carrier is tried.\nfn gated_stdout("
},
{
"path": "src/recover/bash_anchors.rs",
"lines": "9-15",
"snippet": "//! - a READ anchor additionally needs the top-level `toolUseResult` echo with EMPTY\n//! stderr, `interrupted` false, and NO persisted-output pointer (an externalized\n//! stdout is not the inline text). The echo is a per-LANE fact, not a per-level one\n//! (re-measured at CC 2.1.258, v0.10.1): WORKFLOW lanes never carry it (0 of 3867\n//! Bash results), built-in Task/Agent and teammate lanes carry it 97-99% of the\n//! time, the main lane always - so read anchors reach every lane that carries the\n//! echo, by construction, while WRITE anchors (input-side) work in every lane;"
},
{
"path": "src/recover/bash_anchors.rs",
"lines": "300-314",
"snippet": " let tur = rec.tool_use_result_value()?;\n let stdout = tur.get(\"stdout\").and_then(serde_json::Value::as_str)?;\n let stderr_clean = tur\n .get(\"stderr\")\n .and_then(serde_json::Value::as_str)\n .is_none_or(|s| s.trim().is_empty());\n let interrupted = tur\n .get(\"interrupted\")\n .and_then(serde_json::Value::as_bool)\n .unwrap_or(false);\n let persisted = tur.get(\"persistedOutputPath\").is_some();\n if stderr_clean && !interrupted && !persisted {\n return Some((*line_no, rec.timestamp.clone(), stdout.to_string()));\n }\n return None;"
}
],
"instrument": "`rg -c '\"interrupted\":' <a top-level transcript>` versus the same grep on one of its subagent transcripts (expect zero on the subagent). Counting rule: Bash tool_result records carrying the echo, per lane.",
"located": {
"claude_code": null,
"csift": "0.9.4",
"source": "src/recover/bash_anchors.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "inline python3 census over $P/*/subagents/**/*.jsonl (journal.jsonl excluded), splitting each transcript by on-disk shape -- 'workflow' when the path contains /subagents/workflows/, else 'builtin' -- pairing every Bash tool_use id to its tool_result carrier and testing whether that carrier has a top-level toolUseResult object; re-run filtered to records whose `version` field is exactly 2.1.258. Cross-checked live against this verifier's own workflow-lane transcript resolved with `csift whoami @trap:<marker>`.",
"observed": "Restricted to records stamped version 2.1.258: builtin subagent lanes 283 Bash tool_results WITH a toolUseResult object vs 3 without (99.0% carry the echo); workflow subagent lanes 0 with vs 1302 without (0.0%). Across all versions in scope: builtin 2547 with / 86 without (96.7%) -- split further as built-in Task lanes 2222 with / 83 without and teammate lanes 325 with / 3 without -- against workflow 0 with / 3867 without. The keys observed on a built-in subagent Bash result are ['interrupted','isImage','noOutputExpected','stderr','stdout']. Live control: this verifier's own workflow lane at 2.1.258 showed 15 of 15 Bash tool_results with no toolUseResult (and 1 Read likewise); the top-level parent transcript in the same session carried the echo on 2193 of 2193 Bash results.",
"rule": "One observation per Bash tool_result record (tool_result block whose tool_use_id resolves to a Bash tool_use block), bucketed by the carrying transcript's on-disk lane shape. echo_rate = records with a dict-valued top-level toolUseResult over all Bash tool_results in that bucket.",
"note": "This is a decisive refutation of the claim as written, reproducible in one command on current Claude Code, and it is not a version effect -- built-in lanes carried the echo continuously from 2.1.191 through 2.1.258 while workflow lanes never did. The csift consequence is a conservative miss, not a correctness bug: recover's READ content anchors are gated to lanes carrying the echo and are currently declared top-level-only, so read anchors that built-in Task/Agent and teammate lanes could legitimately support are being forgone. WRITE anchors are input-side and are unaffected in every lane. The gate itself is sound wherever it fires; only its stated reach is too narrow. Code site confirmed verbatim at src/recover/bash_anchors.rs lines 9-13."
}
]
},
{
"id": "BASH-013",
"area": "bash-shell",
"behavior": "Claude Code stores a Bash command verbatim in the tool_use block's `input.command`, so a quoted-delimiter heredoc's body sits byte-exact inside that string; 444 of 456 measured heredocs (97.4%) use a quoted delimiter, which makes the body a literal already present in the transcript.",
"depends": "csift's `recover` content anchors take a quoted-heredoc body as a full write anchor, and admit an unquoted delimiter only when the body is free of `$`, backticks and backslashes. If the harness ever normalized or truncated `input.command`, the anchor would fabricate file bytes.",
"code": [
{
"path": "src/bash_mutations/anchors.rs",
"lines": "4-8",
"snippet": "//! A small closed set of shell shapes carries DETERMINISTIC file content in the\n//! transcript itself: a quoted-delimiter heredoc's body is byte-verbatim in the\n//! tool_use input; `cat <file>` / `head -n N <file>` / `sed -n 'A,Bp' <file>` stdout\n//! (under the caller's completeness gate) IS the file window; `echo`/`printf` with\n//! purely literal arguments write known bytes; `truncate -s 0` writes the empty file."
},
{
"path": "src/bash_mutations/anchors.rs",
"lines": "30-31",
"snippet": "//! - An unquoted heredoc delimiter admits the body only when the body is free of\n//! `$`, backticks, and backslashes (bash would expand them)."
}
],
"instrument": "python over one project's transcripts: for Bash tool_use blocks whose `input.command` contains `<<`, count how many use a quoted delimiter (`<<'X'` or `<<\"X\"`). Counting rule: one heredoc per `<<` occurrence in a command string.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6.7; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "LIVE: ran a Bash call writing a quoted-delimiter heredoc (`cat > <tmpfile> <<'HDEOF'`) whose body contains $HOME, a backtick pair, a literal backslash-n, a tab and both quote kinds; then an inline python3 that re-reads the recording transcript, extracts the heredoc body from that tool_use block's input.command with re.search(r\"<<'HDEOF'\\n(.*?)\\nHDEOF\\n\", cmd, re.S), and md5-compares it to the bytes on disk. CENSUS: inline python3 over $P/*.jsonl matching <<-?\\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*)) in every Bash tool_use input.command, skipping <<< here-strings.",
"observed": "Live: transcript body 72 bytes, disk file 72 bytes, transcript md5 72806c7cf4d3b5cbe842af305424d1a7, disk md5 72806c7cf4d3b5cbe842af305424d1a7, BYTE-EXACT True -- the expansion-triggering characters survived verbatim on both sides. Census: 4596 Bash tool_use blocks scanned, 775 of them contain at least one heredoc opener, 978 openers total; 949 quoted (97.0%), 29 bare (3.0%).",
"rule": "One heredoc per << occurrence in a Bash tool_use input.command (<<< here-strings excluded), deduplicated by tool_use block id so a record repeated across the file counts once. Quoted = the delimiter is wrapped in single or double quotes.",
"note": "Both halves confirmed by instrument. The byte-exactness half is the load-bearing one for recover's write anchors and it was tested directly rather than inferred: a body deliberately loaded with $, backtick, backslash, tab and quotes round-tripped through input.command with an identical md5, so Claude Code performs no normalization, escaping or truncation of the command string. Code sites confirmed verbatim at src/bash_mutations/anchors.rs lines 4-8 and 30-31."
}
]
},
{
"id": "BASH-014",
"area": "bash-shell",
"behavior": "The ssh share does not reproduce: measured 0.20% of quoted-heredoc-bearing Bash records corpus-wide (26 of 13101) and 0.9% of openers in one project directory (9 of 978), against the claimed ~16%. The dominant non-file-content heredoc shape is the INTERPRETER heredoc, not the ssh payload -- 615 of 978 openers (62.9%) in scope are fed to python3, versus 279 to cat or tee. Corpus-wide the same ordering holds (2478 cat/tee-bearing records against 423 interpreter-bearing ones under narrower adjacency, with ssh two orders of magnitude below both).",
"depends": "csift admits a heredoc as a write anchor only when its CONSUMER is `cat` or `tee`, which excludes both shapes and enforces nesting depth 0 by construction; admitting nested or interpreter heredocs is the single largest fabrication trap measured.",
"code": [
{
"path": "src/bash_mutations/anchors.rs",
"lines": "26-29",
"snippet": "//! - A heredoc anchors only when its CONSUMER is `cat` or `tee` (a plain local file\n//! write). An interpreter heredoc (`python3 <<EOF`) is a SCRIPT, not file content,\n//! and an `ssh`-fed heredoc writes on a REMOTE filesystem - the consumer gate\n//! excludes both, which also enforces nesting depth 0 by construction."
}
],
"instrument": "`csift files <target> --by timeline` on a heredoc-heavy session, cross-checked against the raw commands: local heredoc writes are attributed, ssh-nested ones are not. Counting rule: one write per heredoc opener; nesting depth counted per command.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "SPEC.md section 6.7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search \"<<-?'[A-Za-z_]\" -t agent.tool.use --count-by tool AND csift search \"ssh [^\\n]{0,120}<<-?'?[A-Za-z_]\" -t agent.tool.use --count-by tool AND csift search \"(python3?|node|bash|sh|perl|ruby|psql|jq) <<-?'?[A-Za-z_]\" -t agent.tool.use --count-by tool AND csift search \"(cat|tee)( -a)? [^\\n]{0,80}<<-?'?[A-Za-z_]\" -t agent.tool.use --count-by tool (all corpus-wide over ~/.claude/projects), plus the per-opener inline python3 consumer census over $P/*.jsonl",
"observed": "Corpus-wide, Bash key only: 13101 records carry a quoted heredoc opener; 26 records carry an ssh-fed heredoc opener = 0.20%; 423 carry an interpreter-fed opener; 2478 carry a cat/tee-fed opener. Per-opener in $P: of 978 openers, the resolved consumer token is python3 on 615 (62.9%), cat on 276 (28.2%), python on 18, bash on 9, tee on 3; only 9 openers (0.9%) have `ssh` anywhere on the opener's own line.",
"rule": "csift figures count RECORDS matching the regex under -t agent.tool.use, bucketed by the tool key (Bash row only), so a record with two heredocs counts once -- they are share-of-records, not share-of-openers. The python figures count one opener per << occurrence and resolve the consumer as the last recognized interpreter/writer token on the opener's logical line.",
"note": "Claude Code's behavior has not changed -- heredocs are still stored verbatim in input.command and an ssh-fed one still targets a remote filesystem -- so this is a corpus-statistics correction, not drift. The depends clause survives intact and is if anything better supported: the single cat/tee consumer gate excludes the interpreter shape (the large trap, 62.9% of openers) and the ssh shape (the small one) by the same rule, and enforces nesting depth 0 by construction. Restating the claim around the interpreter share rather than the ssh share would make it both true and more load-bearing. Code site confirmed verbatim at src/bash_mutations/anchors.rs lines 26-29."
}
]
},
{
"id": "BASH-015",
"area": "bash-shell",
"behavior": "Claude Code hoists exactly one deterministic bash-danger class - `dangerous-rm`, an `rm`/`rmdir` whose target is a bare `$VAR/...` or `${VAR}/...` - to a human approval prompt EVEN under bypass permissions, as a `classifierApprovable:false` safety check. The classifier is a purely static regex path with no LLM and no filesystem access, and it is PURELY LEXICAL: it never checks whether the variable is actually empty.",
"depends": "csift ports the classifier 1:1 so `agents` can split a frozen lane's `pending_classification` into `escalation-blocked` - the one state the jsonl can positively confirm - versus `awaiting-execution`; \"improving\" the port to resolve variables would diverge from the real hoist decision and mispredict whether Claude Code actually blocks.",
"code": [
{
"path": "src/bash_danger.rs",
"lines": "1-6",
"snippet": "//! Faithful port of Claude Code's `dangerous-rm` bash classifier - the ONE deterministic\n//! bash-danger class CC hoists to a human approval prompt EVEN under bypass-permissions (a\n//! `classifierApprovable:false` safetyCheck). Used by `agents` to tell a frozen lane that is\n//! **escalation-blocked** (a pending Bash tool_use CC would hoist → waiting for a human \"Yes\")\n//! apart from one merely **awaiting-execution** (a slow tool) - a distinction the jsonl alone\n//! otherwise can't make (the escalation lives only in CC process memory; see `subagent.rs`)."
},
{
"path": "src/bash_danger.rs",
"lines": "8-14",
"snippet": "//! **Extracted 1:1 from the CC 2.1.193 Mach-O** (function `Ywa` + the `egp`/`Zhp` regexes),\n//! re-grepped at port time (`Dangerous rm operation` / `possibly-empty variable path` strings +\n//! the two regexes verbatim). CC flags `rm`/`rmdir` whose target is a bare `$VAR/…` / `${VAR}/…`.\n//! It is PURELY LEXICAL: it does NOT check whether the variable is actually empty - CC knowingly\n//! accepts that false-positive rate (it would rather over-prompt on `rm $VAR/…`). We MIRROR that\n//! faithfully; do NOT \"improve\" it to resolve variables, or csift's verdict diverges from CC's\n//! real hoist decision and we'd mispredict whether CC actually blocks."
}
],
"instrument": "Grepping the literal 'Dangerous rm operation' no longer finds anything -- the reason string is assembled from a template. Grep `possibly-empty variable path` (still literal, two forms) or the template `Dangerous ${` instead.",
"located": {
"claude_code": "2.1.193",
"csift": "0.1.0",
"source": "AGENTS.md section 3.9; src/bash_danger.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 $B | rg -n 'Dangerous rm operation|possibly-empty variable path' AND strings -n 6 $B | rg -o 'Dangerous [^\"`,]{0,60}operation[^\"`,]{0,50}' | sort -u AND strings -n 6 $B | rg -o 'function sF\\(.{0,400}' AND strings -n 6 $B | rg -o 'dangerousRemoval:\\{[^}]*\\}' AND strings -n 6 $B | rg -o 'circuitBreaker:\"[a-zA-Z]+\"' | sort -u AND strings -n 6 $B | rg -o 'function Po\\(.{0,300}'",
"observed": "The safety check is built verbatim by: function sF(e,n,r){return{behavior:\"ask\",message:n,decisionReason:{type:\"safetyCheck\",reason:`Dangerous ${e} operation ${r}`,classifierApprovable:!1,circuitBreaker:\"dangerousRemoval\"},suggestions:[]}}. The bypass table reads dangerousRemoval:{bypassImmune:!0,classifierRouted:!0} and is consumed by function TUe(e){return e.circuitBreaker!==void 0&&_lr[e.circuitBreaker]?.bypassImmune===!0}; the only other bash-reachable breakers are backgroundOperator:{bypassImmune:!1,classifierRouted:!0} and suspiciousWindowsPath:{bypassImmune:!1,classifierRouted:!0}. The matcher is pure regex with no emptiness resolution: function hnt(e){if(!e.includes(\"$\")||!/\\brm(?:dir)?\\b/.test(e))return null; ... if(yto.test(I))return{command:_,target:I}}. The strings 'on possibly-empty variable path: ' and 'on possibly-empty variable path inside command substitution: ' are both present. A literal 'Dangerous rm operation' is NOT present (rg -c returned no match); the four template forms present are 'Dangerous ${e} operation ${r}', 'Dangerous ${_} operation detected: '${v}'', 'Dangerous ${e} operation detected: '${U}'' and 'Dangerous ${F.command} operation detected inside command substitution: '${F.target}''. The surrounding removal checker _9 opens with let{resolvedPath:v}=Po(ce(),r), and function Po(e,n){...try{let i=e.realpathSync(n);return{resolvedPath:i,isSymlink:i!==n,isCanonical:!0}}catch...}.",
"rule": "String presence or absence in the 2.1.258 binary, one occurrence per distinct site; the bypass verdict is read off the single _lr breaker table entry for dangerousRemoval, and the fs verdict off whether a realpath call is reachable from the checker's entry.",
"note": "The load-bearing core all holds on current Claude Code and was confirmed by instrument rather than by reading: classifierApprovable:!1 is verbatim in sF; dangerousRemoval is the ONE bash-reachable breaker with bypassImmune:!0, so it is still the single class hoisted to a human even under bypass permissions (backgroundOperator and suspiciousWindowsPath are both bypassImmune:!1); and the matcher is purely lexical with no emptiness check, exactly as the port mirrors. Code sites confirmed verbatim at src/bash_danger.rs lines 1-6 and 8-14."
}
]
},
{
"id": "BASH-016",
"area": "bash-shell",
"behavior": "The hoist decision is two regexes: a clause head accepting optional `VAR=val` assignments, an optional backslash and an optional `path/` prefix before `rm` or `rmdir` at a word boundary, and a removal TARGET matching a bare `$VAR` or `${VAR}` (optionally double-quoted) immediately followed by `/` and then one of `*`, `$`, `/`, a quote, or end of string.",
"depends": "csift's `EGP` and `ZHP` are verbatim ports of those two regexes, with the two lookaround-using preprocessing transforms hand-ported because the Rust regex crate has no lookaround; drift here changes which frozen lanes csift calls escalation-blocked.",
"code": [
{
"path": "src/bash_danger.rs",
"lines": "28-33",
"snippet": "/// `egp` (verbatim): a clause head - optional `VAR=val ` assignments, optional backslash, optional\n/// `path/` prefix, then `rm`/`rmdir` at a word boundary. Capture group 1 = `rm` | `rmdir`.\nstatic EGP: LazyLock<Regex> = LazyLock::new(|| {\n Regex::new(r\"^(?:[A-Za-z_][A-Za-z0-9_]*\\+?=[^\\s]*\\s+)*\\\\?(?:[^\\s=]*/)?(rm|rmdir)(?:\\s|$)\")\n .expect(\"egp\")\n});"
},
{
"path": "src/bash_danger.rs",
"lines": "35-41",
"snippet": "/// `Zhp` (verbatim): a removal TARGET that is a bare `$VAR`/`${VAR}` (optional surrounding `\"`)\n/// immediately followed by `/` then one of `* $ / \" ' <end>` - i.e. `$VAR/…`. This is what makes\n/// `\"$SCRATCH/$f\"` dangerous. Lexical only (no emptiness check).\nstatic ZHP: LazyLock<Regex> = LazyLock::new(|| {\n Regex::new(r#\"^\"?\\$(?:\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)\"?/(?:\\*|\\$|/|[\"']|$)\"#)\n .expect(\"Zhp\")\n});"
}
],
"instrument": "`strings` over the installed Claude Code binary for 2.1.258 grepping `rm|rmdir` surfaces the same two regex bodies; the port is pinned by the unit tests in `src/bash_danger.rs`. Counting rule: a command is dangerous when the head regex matches a clause and the target regex matches one of that clause's operands.",
"located": {
"claude_code": "2.1.193",
"csift": "0.7.0",
"source": "AGENTS.md section 3.9; src/bash_danger.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 $B | rg -o '\\^\\(\\?:\\[A-Za-z_\\]\\[A-Za-z0-9_\\]\\*.{0,70}rm\\|rmdir.{0,15}' | sort -u AND strings -n 6 $B | rg -o '\\^\"\\?\\\\\\$\\(\\?:.{0,90}' | sort -u AND sed -n '28,41p' src/bash_danger.rs",
"observed": "Binary head regex: ^(?:[A-Za-z_][A-Za-z0-9_]*\\+?=[^\\s]*\\s+)*\\\\?(?:[^\\s=]*\\/)?(rm|rmdir)(?:\\s|$) -- bound in source as _to. Binary target regex: ^\"?\\$(?:\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)\"?\\/(?:\\*|\\$|\\/|[\"']|$) -- bound as yto. csift EGP: ^(?:[A-Za-z_][A-Za-z0-9_]*\\+?=[^\\s]*\\s+)*\\\\?(?:[^\\s=]*/)?(rm|rmdir)(?:\\s|$). csift ZHP: ^\"?\\$(?:\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)\"?/(?:\\*|\\$|/|[\"']|$). Each binary body appears twice in the strings output (the standalone string and the source-line context). The use site is: let f=d.match(_to); ... let _=f[1]===\"rmdir\"?\"rmdir\":\"rm\" ... if(yto.test(I))return{command:_,target:I}.",
"rule": "Character-for-character equality of the two regex bodies after removing JavaScript regex-literal escaping of the forward slash (\\/ in the binary, / in Rust), which is a lexical artifact of the literal syntax and not a pattern difference. Capture-group semantics checked at the use site: group 1 of the head regex selects rm vs rmdir; the target regex is applied to each of that clause's operands.",
"note": "Both regexes are still byte-identical to the ported EGP/ZHP on current Claude Code, and the described semantics match the use site: the head regex accepts leading VAR=val assignments, an optional backslash and an optional path/ prefix before rm or rmdir at a word boundary, and the target regex accepts a bare $VAR or ${VAR}, optionally double-quoted, immediately followed by / and then one of * $ / a quote or end-of-string. Code sites confirmed verbatim at src/bash_danger.rs lines 28-33 and 35-41."
}
]
},
{
"id": "BASH-017",
"area": "bash-shell",
"behavior": "By 2.1.228 the dangerous-rm classifier had evolved past the generation csift ported: it strips `$(...)` and `(...)` groups to a FIXPOINT (the port is single-pass), and a tree-sitter pass bails to explicit approval when a command carries more than 64 command substitutions.",
"depends": "csift's escalation-blocked prediction can therefore diverge from current Claude Code on those command shapes; the divergence is recorded in-code as a staleness note and a port-refresh follow-up rather than silent drift. The same lexical bash classifier deliberately does not run on Windows `PowerShell` records, where a pending lane classifies `awaiting-execution`.",
"code": [
{
"path": "src/bash_danger.rs",
"lines": "84-91",
"snippet": "/// STALENESS NOTE (binary evidence, 2026-08-12): CC 2.1.228's classifier (`aLa`) has\n/// EVOLVED past the 2.1.x generation this port mirrors - it strips `$(…)` groups to a\n/// FIXPOINT (this port is single-pass), and a tree-sitter pass bails to explicit approval\n/// when a command carries >64 command substitutions. csift's escalation-blocked prediction\n/// can therefore diverge from current CC on those shapes; a port refresh is a recorded\n/// follow-up, not silent drift. (CC also ships a separate Windows `PowerShell` tool; this\n/// lexical-bash classifier deliberately does NOT run on PowerShell commands - a pending\n/// PowerShell lane classifies awaiting-execution.)"
}
],
"instrument": "`strings` over the installed Claude Code binary for 2.1.258, reading the decompiled logic around `Dangerous rm operation` for the fixpoint strip loop and the substitution-count bail, then comparing against `src/bash_danger.rs`. Counting rule: command substitutions per command string versus the 64 threshold; presence or absence of the fixpoint loop on each side.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.4",
"source": "SPEC.md section 6 v0.7.4 ledger item 3; src/bash_danger.rs staleness note"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 $B | rg -o 'function hnt\\(.{0,900}' AND strings -n 6 $B | rg -o 'async function Bno\\(.{0,1200}' AND strings -n 6 $B | rg -c '\"PowerShell\"' AND strings -n 6 $B | rg -o 'circuitBreaker:\"suspiciousWindowsPath\".{0,60}' AND sed -n '84,91p' src/bash_danger.rs",
"observed": "Fixpoint strip, verbatim inside hnt: for(let o=\"\";o!==r;)o=r,r=r.replace(/\\$\\([^()]*\\)/g,\" \").replace(/(?<!\\$)\\([^()]*\\)/g,\" \"); -- an unbounded loop that strips BOTH $(...) and non-$ (...) groups. A second, bounded fixpoint lives in the AST pass: for(let W=\"\",z=0;W!==B&&z<16;z++)W=B,B=B.replace(/\\$\\([^()]*\\)/g,\"__CMDSUB__\");. Substitution bail, verbatim: if(o.length>64){if(/\\brm(?:dir)?\\b/.test(e.text))return sF(\"rm\",`This command contains ${o.length} command substitutions \\u2014 too many to analyze for catastrophic removals. This requires explicit approval.`,`\\u2014 too many command substitutions to analyze (${o.length})`);return null} -- o is filled by a tree walk collecting nodes whose type is command_substitution or process_substitution. PowerShell separation: the literal \"PowerShell\" appears 13 times; a distinct PowerShell destructive-pattern table exists beside the bash path (entries keyed (Remove-Item|rm|del|rd|rmdir|ri) with -Recurse and/or -Force, categories remove_item_recursive_force / remove_item_recursive / remove_item_force) along with a deny path `Remove-Item on system path '${e}' is blocked.` carrying its own classifierApprovable:!1.",
"rule": "Presence or absence of the fixpoint loop and of the substitution-count bail on each side; the threshold is read off the literal comparison o.length>64 where o is the collected substitution-node list, one entry per command_substitution or process_substitution node in the parsed command.",
"note": "The claim's substance holds on current Claude Code: both divergences the staleness note records are still present at 2.1.258, so csift's port remains single-pass against a fixpoint stripper and knows nothing of the substitution-count bail, and the recorded divergence is real rather than resolved. The PowerShell half of the depends clause also holds -- Claude Code ships a separate PowerShell tool with its own destructive-pattern classifier, so declining to run the lexical bash classifier on PowerShell records matches the harness rather than under-reading it. Code site confirmed verbatim at src/bash_danger.rs lines 84-91."
}
]
},
{
"id": "BASH-018",
"area": "bash-shell",
"behavior": "A pending tool approval lives ONLY in Claude Code process memory - it is in neither the transcript nor any control file - so on disk an escalation-blocked lane, a slow tool and a wedged one share ONE signature: the assistant's `tool_use` record (`stop_reason:\"tool_use\"`) is the last record, with no `tool_result` for its `tool_use_id`, and the block carries only `{type, id, name, input}` with no permission or escalation field.",
"depends": "csift's lifecycle forces `status:\"running\"` whenever the newest meaningful record is an unreturned `tool_use` (never `completed`) and refuses to distinguish slow from wedged; `status`/`wait` state outright that the pending permission prompt is invisible.",
"code": [
{
"path": "src/subagent/lifecycle.rs",
"lines": "35-40",
"snippet": " // TAIL: last record's timestamp == completion (best-effort), whether the transcript\n // terminates with a visible assistant message (a clean finish), AND whether the lane is\n // FROZEN at an unreturned tool_use. The frozen verdict comes from the NEWEST meaningful\n // record only (the first non-metadata record from EOF): if it is an assistant tool_use, no\n // tool_result followed it (it IS the last record) ⇒ the lane is blocked there, NOT done. The\n // terminal_agent_msg walk-back is UNCHANGED for every non-frozen lane."
},
{
"path": "src/live/tail.rs",
"lines": "3-5",
"snippet": "//! Reads the FINAL window of a transcript (bounded, never the whole file), walks it\n//! backward, and reports the liveness-relevant shape: the newest UNRETURNED tool call\n//! (a use whose id has no later result = a tool in flight, or a process dead mid-tool),"
}
],
"instrument": "`csift search \"\" <target> --count-by pairing` (the `pending` bucket) plus `csift agents <target> --format json | jq 'select(.kind==\"agent\" and .status==\"running\") | {pending_tool_name, pending_classification, pending_since_utc}'`. Counting rule: one record per pairing bucket; one node per lane.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md section 3.9; SKILL.md status honesty limits"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c 'json.load(open(one ~/.claude/sessions/<pid>.json)); print top-level keys' AND rg -io 'pending[A-Za-z]*|permission[A-Za-z]*|approval[A-Za-z]*|toolUse[A-Za-z]*' ~/.claude/sessions/*.json | sort -u AND rg -il 'pendingApproval|permissionRequest|awaitingApproval|classifierApprovable' ~/.claude/tasks AND an inline python3 census of tool_use block key sets over $P/*.jsonl AND csift status @<session> AND csift search \"\" @trap:<marker> --count-by pairing",
"observed": "Session registry: the 19 top-level keys of a ~/.claude/sessions/<pid>.json are pid, sessionId, cwd, startedAt, procStart, version, peerProtocol, peerFeatures, kind, entrypoint, pidDomain, messagingSocketPath, name, nameSource, nameSince, updatedAt, status, statusUpdatedAt, bridgeSessionId -- the case-insensitive rg for pending/permission/approval/toolUse across every file in ~/.claude/sessions returned nothing, and the rg across ~/.claude/tasks (167 task directories) returned nothing. Block census: 7798 of 7798 tool_use blocks carry exactly ONE key set, ('caller','id','input','name','type'); no permission, escalation, isEscalated or requires_approval key appears on any block. Live: with this verifier's own lane frozen mid-call, csift status printed `child <agent-id> in-flight unreturned Bash call (0s ago)` and `tail no pending call; last stop_reason tool_use`, and csift search \"\" @trap:<marker> --count-by pairing on that same lane printed `88 paired` / `1 pending`.",
"rule": "One key set per tool_use block, unioned across every top-level transcript in $P; a claim of an approval-state field is refuted only if zero blocks carry one. For the control files, one grep per file over the whole of ~/.claude/sessions and ~/.claude/tasks. The pairing census counts one record per bucket, so the pending bucket is the count of tool_use records whose id has no tool_result in scope.",
"note": "Every half was confirmed by an instrument that ran. The signature is exactly as described and was reproduced live rather than reconstructed: a frozen lane appears on disk only as a trailing unreturned tool_use, and csift reports it as `pending` on the pairing axis and `in-flight` in status, with no field anywhere on the record, in the session registry, or under ~/.claude/tasks that would separate an escalation-blocked lane from a slow or wedged one. The three-states-one-signature honesty constraint therefore still binds. Code sites confirmed verbatim at src/subagent/lifecycle.rs lines 35-40 and src/live/tail.rs lines 3-5."
}
]
},
{
"id": "BASH-019",
"area": "bash-shell",
"behavior": "Claude Code lexically recognizes read-shaped Bash commands and SEEDS its readFileState from them, so a bash `cat <file>` legalizes a later Write of that file; a Bash command never invalidates readFileState.",
"depends": "csift's `recover` treats a gated bash `cat`/`head -n N`/`sed -n 'A,Bp'` stdout as a read anchor for the same reason the harness does, and its freshness reasoning assumes bash writes create staleness rather than clearing it.",
"code": [
{
"path": "src/bash_mutations/anchors.rs",
"lines": "12-14",
"snippet": "//! ADMISSION LAWS (each refusal falls back to today's behavior, never a wrong anchor):\n//! - A READ anchor demands a SINGLE simple segment: a compound command's stdout is a\n//! concatenation nothing can attribute to one file."
}
],
"instrument": "Find in the corpus a Write that immediately follows a bash `cat` of the same path with no intervening Read and no rejection (`is_error:true`). Counting rule: one witness sequence per session.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 4.9; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 $B | rg -o 'async function hWn\\(.{0,1400}' AND strings -n 6 $B | rg -o 'function Jzo\\(.{0,1500}' AND strings -n 6 $B | rg -o '.{0,300}returnCodeInterpretation.{0,200}' (for the call site)",
"observed": "Call site, verbatim on the Bash result path: if(!me&&!nt&&!ke.backgroundTaskId)await hWn(e.command,n.readFileState,C.signal,ke.code,...) -- the command string and the readFileState map are handed to the seeder, skipped only when the result was interrupted, was an image, or was backgrounded. Seeder, verbatim: async function hWn(e,n,r,o,d){let f=Jzo(e).filter((v)=>!v.requiresExitZero||o===0);if(f.length===0)return;let _=ce();await Promise.all(f.map(async(v)=>{let C=ct(v.filePath);if(n.get(C))return;try{let I=await _.stat(C);if(I.size>10485760)return;if(r.aborted)return;let F=await _.readFile(C,{encoding:\"utf8\"}),B=AA(F),U=dqo(B,v);if(U===null)return;n.set(C,{content:U.content,timestamp:Math.floor(I.mtimeMs),offset:U.offset,limit:U.limit,...})}catch{}}))}. Recognizer: function Jzo(e){if(/[|<>]/.test(e))return[]; ...} dispatching to a sed arm that requires -n/--quiet/--silent and an A,Bp or Np expression while rejecting -i/--in-place/-e, and to a table eqo=new Map([[\"cat\",new Set([\"-n\",\"--number\"])],[\"nl\",new Set],[\"bat\",new Set([\"-n\",\"--number\",\"-p\",\"--plain\"])],[\"batcat\",new Set([\"-n\",\"--number\",\"-p\",\"--plain\"])]]) that accepts exactly one non-flag operand.",
"rule": "Presence of a write into the readFileState map (n.set) and absence of any delete or invalidate on that map anywhere in the Bash result path, read off the seeder body; the read shapes are enumerated from the recognizer's dispatch arms rather than assumed.",
"note": "Both halves confirmed at the binary. The seeding half is verbatim -- n.set(C,{content,timestamp,offset,limit}) writes the readFileState entry a later Write or Edit gates on. The never-invalidates half is confirmed structurally by the guard `if(n.get(C))return`: an existing entry is left untouched, and the function contains no delete or clear of the map at all, so a Bash command can only add freshness, never remove it. Two further gates worth recording alongside: a file larger than 10485760 bytes is skipped, and the recognizer bails outright (returns the empty list) when the command contains |, < or >, so a piped or redirected cat seeds nothing. That last gate is a close structural analogue of csift's own single-simple-segment admission law for read anchors. Code site confirmed verbatim at src/bash_mutations/anchors.rs lines 12-14."
}
]
},
{
"id": "BASH-020",
"area": "bash-shell",
"behavior": "Claude Code routinely runs Bash commands that mutate files whose names never appear in the command text - whole-tree formatters (`cargo fmt --all`), package managers, archive extraction and patch application - so the mutated file set exists only in the tool's own discovery, the lockfile layout, or the archive/diff contents.",
"depends": "csift emits these as CLASS MARKERS (`fmt:cargo`, `pkg:npm`, `extract:tar`, `interp:python`, `git:<sub>`) that are never resolved, joined or path-matched, and `recover` counts them per window as opaque mutating activity so a window is never falsely reported clean; a formatter that DOES name operands emits ordinary path rows instead, and dry-run forms emit nothing.",
"code": [
{
"path": "src/bash_mutations/classes.rs",
"lines": "1-11",
"snippet": "//! Mutating-CLASS markers: commands known to rewrite files without naming them.\n//!\n//! A formatter (`cargo fmt`), a package manager (`npm install`), an archive extraction\n//! (`tar -xf`), and a patch application all mutate real files whose names never appear\n//! in the command text: the file set comes from the tool's own discovery, the lockfile\n//! layout, or the archive/diff contents. No lexical parser can name those files, and\n//! fabricating names would break the precision contract. So these commands are\n//! reported as CLASS MARKERS in the `git:<sub>` style: a pseudo-path (`fmt:cargo`,\n//! `pkg:npm`, `extract:tar`) that flags WHAT KIND of mutation ran, deliberately not\n//! WHICH files. Markers are never resolved, joined, or matched against a `--file`;\n//! `recover` counts them per window as opaque mutating activity."
}
],
"instrument": "`csift recover <target> --file <path> --coverage` on a session that ran a formatter: the opaque-command disclosure names class markers with a per-window count. Counting rule: one marker per parsed mutating-class segment.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "AGENTS.md section 3.11; src/bash_mutations/classes.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift files @<session> --by timeline --no-subagents | rg -o '(fmt|pkg|extract|interp|git):[a-z-]+' | sort | uniq -c | sort -rn AND csift files @<session> --by file --format json --no-subagents | rg -o '\"path\":\"(fmt|pkg|extract|interp|git):[^\"]*\"' | sort | uniq -c AND csift recover @<session> --file .claude/settings.json --coverage --no-subagents",
"observed": "Timeline over one real session emitted 38 class-marker rows: interp:python x18, git:commit x6, git:add x6, pkg:npm x4, git:stash x3, fmt:prettier x1. The per-file rollup carries each as a `path` value -- \"path\":\"pkg:npm\", \"path\":\"interp:python\", \"path\":\"fmt:prettier\", \"path\":\"git:stash\", \"path\":\"git:commit\", \"path\":\"git:add\" -- and none of them appears as a filesystem path in any row. The recover coverage disclosure printed, verbatim: `opaque in window: 26 mutating-class command(s) whose file set is not in the command text (fmt:prettier, git:stash x3, interp:python x18, pkg:npm x4)`, followed by per-line, per-turn, timestamped rows and a `(+21 more; use the search below)` elision with the exact continuation search.",
"rule": "One marker per parsed mutating-class command segment, counted from the chronological timeline so a compound command contributes one row per mutating segment; the recover figure is the count of such segments falling inside the reconstruction window for the named file.",
"note": "Both halves confirmed by running csift against a real session. The harness half is established by the markers themselves: each row is a real Bash command Claude Code ran whose mutated file set is absent from the command text, and 38 such commands occur in one session. The csift half is confirmed on both surfaces -- the markers are emitted as pseudo-paths and never resolved, joined or matched against a real path, and recover counts them per window under an explicit opaque heading with a per-marker breakdown, so a window containing only such commands is disclosed as opaque rather than reported clean. The elision itself reports its own drop count and hands back the exact continuation command, matching the no-silent-truncation invariant. Code site confirmed verbatim at src/bash_mutations/classes.rs lines 1-11."
}
]
},
{
"id": "BASH-021",
"area": "bash-shell",
"behavior": "Claude Code writes Windows paths into structured tool fields in native form (`C:\\...`), so besides a leading `/` the only absolute operand shapes are a `<letter>:` drive prefix and a `\\\\`-led UNC path.",
"depends": "csift's `is_absolute_shell_path` accepts all three shapes before deciding whether to join an operand to the record `cwd`; treating `C:\\...` as relative would join a Windows absolute path onto a cwd and fabricate a path in every `files`/`recover` row.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "209-215",
"snippet": "pub fn is_absolute_shell_path(p: &str) -> bool {\n if p.starts_with('/') || p.starts_with(\"\\\\\\\\\") {\n return true;\n }\n let b = p.as_bytes();\n b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && matches!(b[2], b'/' | b'\\\\')\n}"
}
],
"instrument": "The unit tests beside `src/bash_mutations/` pin `is_absolute_shell_path` for `/a`, `C:/a`, `C:\\a` and a `\\\\`-led UNC path; on a real Windows session, confirm Edit/Write `filePath` values use the drive form. Counting rule: one path per structured tool input field.",
"located": {
"claude_code": "2.1.228",
"csift": "0.8.0",
"source": "AGENTS.md section 3.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -Fx '^(?:[a-zA-Z]:[\\\\/]|\\\\\\\\)' (and the two sibling predicates); then a python json census of every structured tool-input path field (file_path / filePath / notebook_path / path) over a Windows-recorded Claude Code 2.1.258 transcript tree (5 .jsonl files, encoded project dir of drive-letter shape) copied onto this machine; then cargo test is_absolute_shell_path",
"observed": "Binary carries three fully-qualified-path predicates that enumerate exactly the drive-letter and UNC forms: '^(?:[a-zA-Z]:[\\\\/]|\\\\\\\\)' (its string table continues ': not a fully qualified path'), '^(?:[A-Za-z]:[\\\\/]|[\\\\/]{2})' and '^(?:[A-Za-z]:\\\\|\\\\\\\\)'; the Write tool schema string reads 'The absolute path to the file to write (must be absolute, not relative)'. Windows transcripts: 55 of 55 structured tool-input path values are native drive form matching ^[A-Za-z]:\\\\ (Read file_path 32, Edit file_path 10, Write file_path 9, Grep path 4); 0 were POSIX-ified or relative. All 389 cwd-carrying records in that tree stamp one native drive-form cwd. cargo test: 1 passed, 0 failed.",
"rule": "One path per structured tool input field. Shape buckets: leading '/', ^[A-Za-z]:[\\\\/], leading '\\\\\\\\', else relative. Counted over every tool_use block in the tree.",
"note": "Code site confirmed verbatim at src/bash_mutations/cwd.rs:209-215, unchanged. The drive-letter arm is confirmed on real Windows-recorded data at the current Claude Code version; the UNC arm is confirmed only from the binary's own predicates, since no '\\\\\\\\'-led operand occurred in the observed Windows session. A session on a UNC-mounted share would settle that arm on disk."
}
]
},
{
"id": "BASH-022",
"area": "bash-shell",
"behavior": "Real Bash command shapes are dominated by a leading absolute `cd`: across 118,900 measured commands, 61.16% began with `cd <absolute>` - though only 7.87% of all commands cd'd to the directory the shell was already tracking - 2.65% were backgrounded through the Bash tool's `run_in_background` field and none by a trailing top-level `&`, `pushd`/`popd`/`chdir` appeared zero times as a shell verb against 79,911 `cd` verbs, and a `cd` inside a `( )` subshell appeared twice.",
"depends": "csift still models `pushd`/`chdir` as a `cd` and `popd` as an unknowable restore, but this distribution is why per-command lexical tracking suffices in practice instead of a full shell simulation.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "111",
"snippet": " /// `cd`/`pushd`/`popd`. Call AFTER stamping the segment's mutations."
},
{
"path": "src/bash_mutations/cwd.rs",
"lines": "130-132",
"snippet": " // `popd` restores a directory this layer does not model.\n \"popd\" => self.set(CwdAt::Unknown),\n \"cd\" | \"pushd\" | \"chdir\" => self.apply_cd(operands),"
}
],
"instrument": "python over a cached set of transcripts: for every Bash tool_use, classify the first top-level segment and count leading `cd <abs>` shapes, backgrounded launches and `pushd`/`popd` tokens. Counting rule: one command per tool_use block; segments split on top-level `;`, `&&`, `||`, `|`, `&` and newline outside quotes and heredocs.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md section 4.9; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 walk of every *.jsonl under ~/.claude/projects: for each Bash/PowerShell tool_use block, mask quoted spans and heredoc bodies, split top-level segments on ';', '&&', '||', '|', '&' and newline outside quotes and parens, then classify the first segment's verb and census every cd-family verb and its paren depth",
"observed": "118,900 Bash/PowerShell tool_use commands. Leading 'cd <absolute>': 72,718 = 61.16% (claim: 42.8%). Of those, target equal to the record's own cwd: 9,362 = 7.87% of all commands. Backgrounded by a trailing top-level '&': 0 = 0.00%; backgrounded via the Bash tool's run_in_background field: 3,150 = 2.65% (claim: 9.21%). cd-family segment verbs: cd 79,911 (claim: 11,974), pushd 0, popd 0, chdir 0 (claim: 0 / 0 / 1). cd inside a ( ) subshell: 2 (claim: 1). All 39 Bash commands whose text contains pushd, popd or chdir carry the word inside a heredoc'd script body, never as a shell verb. The corpus is live: the same walk 20 minutes later counted 119,110.",
"rule": "One command per Bash/PowerShell tool_use block. A segment verb is the first token after stripping env assignments and the prefixes command/builtin/exec/env/nohup/time/sudo. A leading 'cd' target is the first non-flag operand; absolute = leading '/', '\\\\\\\\', or ^[A-Za-z]:[/\\\\].",
"note": "Both code sites confirmed verbatim at src/bash_mutations/cwd.rs:111 and 130-132, unchanged. The structural conclusion holds and is stronger than claimed - pushd/popd/chdir are now categorically absent as shell verbs, and subshell cd stays vanishingly rare - so per-command lexical tracking still suffices. Every number needed correction, and one sub-clause did not survive: the claim characterized the leading absolute cd as landing the shell 'where it already was', but only 12.9% of leading-absolute-cd commands (9,362 of 72,718) target the directory the record cwd already names. Backgrounding also moved carrier rather than frequency: the trailing-'&' form has disappeared entirely in favour of the tool's own run_in_background field."
}
]
},
{
"id": "BASH-023",
"area": "bash-shell",
"behavior": "Across 17,361 measured mutation-target operands, resolution classes were 38.40% typed absolute, 52.65% relative - of which only 24.30% resolve at the zero-inference `cwd-joined` class and 75.70% need the lexical `cd-tracked` class - and 8.95% dynamic (`$VAR` or a glob, never resolvable); the `cd-tracked` class agrees with Claude Code's own modified-file hints 99.76% of the time (411 of 412 joinable entries, the one exception being a path-normalization artifact rather than a wrong answer).",
"depends": "csift labels every resolved operand with an explicit `Resolution` class (`absolute`, `cwd-joined`, `cd-tracked`, `unresolved`) and keeps a dynamic operand VERBATIM rather than guessing; collapsing the classes would hide which `files`/`recover` rows involved inference.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "30-39",
"snippet": "//! - `Absolute`: the operand was typed absolute; used as-is.\n//! - `CwdJoined`: a relative operand before any `cd`; joined to the record's own `cwd`\n//! field. That value is data Claude Code wrote, so the join involves no inference.\n//! - `CdTracked`: a relative operand after one or more literal in-command `cd`s;\n//! the join uses the lexically tracked directory. This is inference: a `cd` that\n//! failed at runtime is invisible here. Measured against Claude Code's own\n//! modified-file hints, this class resolves correctly in about 99.7% of cases.\n//! - `Unresolved`: the operand (or a `cd` on the way to it) carries `~`, `$VAR`, a\n//! substitution, or the cwd is otherwise unknowable. The operand is kept VERBATIM\n//! and callers must disclose it as unresolved, never treat it as a full path."
}
],
"instrument": "`csift files <target> --format json | jq -r 'select(.kind==\"mutation\") | .resolution' | sort | uniq -c` gives the class distribution for one session; the 99.7% figure comes from joining `cd-tracked` rows against the same result's `toolUseResult.staleReadFileStateHint` paths. Counting rule: one class per mutation row.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md section 4.9; src/bash_mutations/cwd.rs resolution classes; dev session 2026-08-22"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift files --by timeline --format json (no target, all projects) piped to a python census of the `resolution` field; then a python join of the cd-tracked rows against every toolUseResult.staleReadFileStateHint file list in the same corpus",
"observed": "27,744 bash mutation rows. 10,383 carry resolution null (class-marker pseudo-paths such as interp:/git:/fmt:), leaving 17,361 resolvable operands: absolute 6,667 = 38.40% (claim 19.75%), cd-tracked 6,920 = 39.86%, cwd-joined 2,221 = 12.79%, unresolved 1,553 = 8.95% (claim 17.47%). Relative subtotal 9,141 = 52.65% (claim 62.79%), split cwd-joined 24.30% / cd-tracked 75.70% (claim 84.21% / 15.70% - inverted). Accuracy join: 1,077 records carry toolUseResult.staleReadFileStateHint, 884 parse to a file list, 371 of those calls produced at least one cd-tracked row; of the 412 hint entries sharing a basename with a cd-tracked row, 411 = 99.76% have csift's absolute path ending in the hint's full relative path. The single exception is a '../../'-normalization artifact where csift's resolved path is in fact correct.",
"rule": "One class per mutation row emitted by `csift files --by timeline`; percentages taken over rows with a non-null resolution (null rows are class markers, not file operands). Accuracy: one hint entry per joinable file; agreement = csift's absolute path ends with the hint's relative path.",
"note": "Code site confirmed verbatim at src/bash_mutations/cwd.rs:30-39, unchanged, and its 'about 99.7%' figure reproduces almost exactly at 99.76% - that half of the claim needs no edit. The distribution half inverted: the claim's reassuring reading, that most relative operands land in the zero-inference cwd-joined class, is no longer true - the inference-bearing cd-tracked class now dominates relative operands about 3:1 where the claim had cwd-joined dominating about 5:1. This is consistent with and explained by the BASH-022 shift: leading absolute `cd` rose from 42.8% to 61.16%, so far more operands now sit after a cd. The practical consequence is that a larger share of files/recover rows now rest on inference, which raises the value of the explicit Resolution class label rather than undermining it."
}
]
},
{
"id": "BASH-024",
"area": "bash-shell",
"behavior": "8.39% of 119,110 measured Bash calls mutate a persistent file, and across 397 real `sed -i` invocations (292 commands) `sed -i` yielded ZERO deterministic literal-subset content anchors - even the 55 invocations whose s/// OLD and NEW are both fully literal, because the substitution is conditional on prior content and sed exits 0 whether or not it matched.",
"depends": "csift's bash content anchors deliberately exclude `sed -i`, `tail`, variable targets and interpreter/ssh heredocs; the bash resolution work raised full-target attribution from 27.1% to a measured 77.0% ceiling.",
"code": [
{
"path": "src/bash_mutations/anchors.rs",
"lines": "1-10",
"snippet": "//! Content-ANCHOR classification of one Bash command (the recover layer's v0.9.4\n//! \"bash reads are reads, bash writes are writes\" upgrade).\n//!\n//! A small closed set of shell shapes carries DETERMINISTIC file content in the\n//! transcript itself: a quoted-delimiter heredoc's body is byte-verbatim in the\n//! tool_use input; `cat <file>` / `head -n N <file>` / `sed -n 'A,Bp' <file>` stdout\n//! (under the caller's completeness gate) IS the file window; `echo`/`printf` with\n//! purely literal arguments write known bytes; `truncate -s 0` writes the empty file.\n//! Everything else stays in the boundary/heuristic lanes - correctness first, but\n//! honesty about the decidable subset is not surrender on it."
}
],
"instrument": "Collect every `sed -i` Bash command in the corpus (`csift search 'sed -i' <scope> -t agent.tool.use`) and inspect each for an extractable literal old/new pair. Counting rule: commands carrying a literal replacement subset over all `sed -i` commands; mutating calls over all Bash calls.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger item 1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift files --by timeline --format json (no target, all projects) piped to a python count of distinct (session_id, line) bash mutation rows with a non-null resolution, divided by the python-walk count of Bash/PowerShell tool_use blocks under ~/.claude/projects; then a python extraction of every `sed -i` invocation, its script argument, and its paired tool_result",
"observed": "9,996 distinct Bash calls produced at least one real-path bash mutation row, out of 119,110 Bash/PowerShell tool_use commands = 8.39% (claim: 19.4% of 22,410). Counting class-marker pseudo-path rows as mutations raises it to 13,563 = 11.39%. sed -i: 292 commands, 277 distinct tool_use ids, 397 `sed -i` invocations. Script shapes: 182 s/// with regex metacharacters in both OLD and NEW, 118 non-s/// scripts (d/a/i/p/range), 55 with OLD and NEW both fully literal, 23 OLD-only metacharacters, 8 NEW-only, 11 unparseable. Paired results: 6 errored, 286 returned non-empty stdout - and every non-empty body is another command's output in the same compound (formatter, test runner, version control), never the edited file's bytes. ZERO of the 397 invocations yields a deterministic literal-subset content anchor.",
"rule": "Mutating call = a distinct (session_id, line) carrying at least one bash mutation row with a non-null resolution, over all Bash/PowerShell tool_use blocks in the corpus. A content anchor = a shape from which file bytes are known from the transcript alone; a `sed -i` substitution is conditional on prior content and so can never qualify.",
"note": "Code site confirmed verbatim at src/bash_mutations/anchors.rs:1-10, unchanged. The `sed -i` half of the claim reproduces exactly, and the code shows why structurally rather than incidentally: anchors.rs admits `sed` only under `\"sed\" if words.len() == 4 && words[1].orig == \"-n\"` (src/bash_mutations/anchors.rs:310), the windowed-read idiom, so `sed -i` can never reach the anchor path at all. The mutation rate fell from a claimed 19.4% to a measured 8.39% on a corpus roughly five times larger, so the claim's rate needed correction. Not re-measured here: the `depends` field's attribution figures (27.1% raised to a 77.0% ceiling) - no instrument was run against them, and they should be treated as unverified until one is."
}
]
},
{
"id": "BASH-025",
"area": "bash-shell",
"behavior": "Whether a `Bash` tool_result carries the structured `toolUseResult` OBJECT is a per-LANE and per-VERSION fact, not a property of the tool: a WORKFLOW-agent lane carries it on NONE of its 70,777 Bash results at any version, the current 2.1.258 included (0 of 5,100); a BUILT-IN Task/Agent or teammate lane carries it on none of the 6,837 results written by 2.1.156 through 2.1.177 and on 92.8-100% per version from 2.1.179 onward (12,752 of 19,943 corpus-wide); the main lane carries it on 31,944 of 32,489. The non-object remainder in every lane is either no `toolUseResult` field at all or a STRING-valued one carrying a failed call's error text.",
"depends": "csift's `recover` READ content anchors demand that object echo (a `stdout` string, empty stderr, `interrupted` false, no persisted-output pointer), so a bash read anchor is reachable in the main lane and in a post-2.1.179 built-in or teammate lane but never in a workflow lane or an archived pre-2.1.179 subagent lane; WRITE anchors are input-side and work in every lane. Both blanket readings - `subagent lanes never carry it` and `every current lane carries it` - mis-gate a whole class of transcripts.",
"code": [
{
"path": "src/recover/bash_anchors.rs",
"lines": "12-15",
"snippet": "//! (re-measured at CC 2.1.258, v0.10.1): WORKFLOW lanes never carry it (0 of 3867\n//! Bash results), built-in Task/Agent and teammate lanes carry it 97-99% of the\n//! time, the main lane always - so read anchors reach every lane that carries the\n//! echo, by construction, while WRITE anchors (input-side) work in every lane;"
}
],
"instrument": "Same scan, re-measured 2026-09-02 later in the day: workflow 0 object / 947 string / 69,830 absent; built-in-or-teammate 12,752 object / 336 string / 6,855 absent, every object echo at 2.1.179 or later and zero at 2.1.156-2.1.177; main 31,944 object / 545 string / 0 absent; over 67 top-level and 7,584 subagent transcripts.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.1",
"source": "src/recover/bash_anchors.rs module doc; measured 2026-09-02"
},
"first_seen_claude_code": "2.1.179",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 one-pass census over ~/.claude/projects (walk every *.jsonl except journal.jsonl; per file first register the ids of every `tool_use` block whose name is Bash or PowerShell, then for every later `tool_result` block whose tool_use_id is registered tally the lane, the JSON type of the carrying record's top-level `toolUseResult`, and that record's `version`; lane = workflow when the path has a `subagents/workflows/` component, builtin_or_teammate for any other `subagents/` component, else main)",
"observed": "7,671 transcripts scanned: 67 top-level, 20 elicitation sidecars (0 Bash results), 7,584 subagent. WORKFLOW lane 0 object / 947 string / 69,830 absent = 70,777 Bash tool_result blocks - and 0 object of the 5,100 results written by the CURRENT 2.1.258 (47 string, 5,053 absent), so the zero is live, not archival. BUILTIN-OR-TEAMMATE lane 12,752 object / 336 string / 6,855 absent = 19,943; object count is 0 at 2.1.156 (0 of 956), 2.1.159 (0 of 3,151), 2.1.170 (0 of 15) and 2.1.177 (0 of 2,715) = 6,837 results, and the first object echo appears at 2.1.179 (16 of 16); from 2.1.179 onward the per-version object share runs 92.8% (2.1.199, 885 of 954) to 100%, and is 99.0% (283 of 286) at 2.1.258. MAIN lane 31,944 object / 545 string / 0 absent = 32,489, of which 341 object / 2 string at 2.1.258. Code site src/recover/bash_anchors.rs lines 12-15 matches the claim's snippet verbatim; the read-anchor gate it documents is implemented at lines 300-309 (`stderr_clean && !interrupted && !persisted` over the `toolUseResult` object).",
"rule": "One tally per `tool_result` BLOCK whose `tool_use_id` names a Bash/PowerShell `tool_use` registered earlier in the SAME transcript file. kind = object | string | absent, by the JSON type of the carrying record's top-level `toolUseResult`. Version buckets use the carrying record's own `version`. Percentages are object / (object + string + absent) within one version and lane.",
"note": "The structural claim reproduced exactly: workflow-lane object count is still zero at every version including the current one, the built-in lane's first object echo is still 2.1.179, and the builtin (12,752 of 19,943), 2.1.156-2.1.177 (6,837) and main (31,944 of 32,489) totals are byte-identical to the ledger's. Two numbers needed correction. (1) The workflow denominator grew from 70,046 to 70,777 (942 -> 947 string, 69,104 -> 69,830 absent) because the corpus itself grew between the two measurements on the same date; the object count stayed 0. (2) The per-version floor from 2.1.179 onward is 92.8% (2.1.199), not 93%. Also worth reconciling: the code doc at bash_anchors.rs:12-15 cites `0 of 3867 Bash results` for the workflow lane, a narrower scope than this whole-corpus 70,777 - both are zero, but the two figures are not the same census and a reader may take the smaller one for the corpus total."
}
]
},
{
"id": "BASH-026",
"area": "bash-shell",
"behavior": "The Bash echo's OPTIONAL key set drifts with the harness version and is not closed at any one version: 2.1.258's toolUseResult constructor emits `backgroundedByUser`, `backgroundedByTurnAbort`, `backgroundedToDeliverMessage`, `backgroundEndsWithFinalResponse`, `backgroundCwdHint` and `gitOperation` beside the keys most records show, and its tool_result BLOCK mapper additionally destructures `structuredContent`. Four of those - `backgroundedByUser`, `backgroundedByTurnAbort`, `backgroundedToDeliverMessage`, `structuredContent` - occur on no record anywhere in the corpus; `backgroundEndsWithFinalResponse` occurs on 8, all in a BUILT-IN/teammate subagent lane at 2.1.231 (always true, always beside a `backgroundTaskId`), which is why a top-level-only census reads zero for it. Conversely `assistantAutoBackgrounded` occurs on 21 corpus records - always `false`, always beside a `backgroundTaskId` - confined to versions 2.1.156, 2.1.159 and 2.1.177, and is absent from the 2.1.258 binary entirely: it is the only one of these names with zero binary hits.",
"depends": "csift keeps `toolUseResult` UNPARSED as a `RawValue` and reads only small named fields through the typed probe, whose every field is an `Option<Value>` accepting any JSON type, so an added key and a retired one both parse without error - that tolerance is what lets one binary read a corpus spanning a hundred harness releases. A surface that enumerated this key set from one version's transcripts would be wrong for exactly the archived sessions a forensic tool exists to read.",
"code": [
{
"path": "src/model/record.rs",
"lines": "293-299",
"snippet": "/// Typed probe of the SMALL `toolUseResult` fields the hot paths consult (see\n/// [`Record::tur_probe`]). Every field is an `Option<Value>` - a tiny scalar/map tree\n/// that accepts ANY JSON type, so one oddly-typed field can never fail the whole probe\n/// (each accessor then applies the same `as_str`/`as_bool`/`as_object` coercion the\n/// former `.get(…)` chains did - byte-identical semantics). Crucially, the blob's HUGE\n/// unlisted values (file bodies, stdout echoes, structured patches) are skipped by\n/// serde's ignore path without ever being allocated."
}
],
"instrument": "Corpus census widened from top-level transcripts to EVERY *.jsonl under ~/.claude/projects (7,831 files, 1,312,236 lines), one tally per line containing the quoted key name. Measured 2026-09-02: backgroundedByUser 0, backgroundedByTurnAbort 0, backgroundedToDeliverMessage 0, structuredContent 0, backgroundEndsWithFinalResponse 8, assistantAutoBackgrounded 21.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured 2026-09-02"
},
"first_seen_claude_code": "2.1.156",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'noOutputExpected:Lqo\\(e\\.command\\).{0,600}' and | rg -o 'ResultBlockParam\\(\\{interrupted:.{0,320}' and | rg -c 'assistantAutoBackgrounded'; (b) python3 byte census over ~/.claude/projects counting, per line, occurrences of each quoted key name; (c) python3 per-key tally over the main-lane Bash object echoes from the BASH-025 scan.",
"observed": "(a) the 2.1.258 toolUseResult constructor is `noOutputExpected:Lqo(e.command),backgroundTaskId:ke.backgroundTaskId,backgroundedByUser:ke.backgroundedByUser,backgroundedByTurnAbort:ke.backgroundedByTurnAbort,backgroundedToDeliverMessage:ke.backgroundedToDeliverMessage,timedOutAfterMs:ke.timedOutAfterMs,backgroundEndsWithFinalResponse:fn,backgroundCwdHint:hn,dangerouslyDisableSandbox:...,persistedOutputPath:Ot,persistedOutputSize:gn,staleReadFileStateHint:An,ghRateLimitHint:pn,gitOperation:xn}`; the separate tool_result BLOCK mapper destructures `{interrupted,stdout,stderr,isImage,backgroundTaskId,backgroundedByUser,backgroundedToDeliverMessage,timedOutAfterMs,backgroundEndsWithFinalResponse,backgroundCwdHint,structuredContent,persistedOutputPath,persistedOutputSize,staleReadFileStateHint,ghRateLimitHint}`; `assistantAutoBackgrounded` = 0 matching lines. (b) over 7,831 jsonl files / 1,312,236 lines: \"backgroundedByUser\" 0, \"backgroundedByTurnAbort\" 0, \"backgroundedToDeliverMessage\" 0, \"structuredContent\" 0, \"backgroundEndsWithFinalResponse\" 8, \"assistantAutoBackgrounded\" 21, \"backgroundCwdHint\" 1204, \"gitOperation\" 827, \"returnCodeInterpretation\" 251. The 8 `backgroundEndsWithFinalResponse` records are all `type:\"user\"` tool_result carriers in a BUILT-IN/teammate subagent lane at version 2.1.231, value always true, each beside a `backgroundTaskId`. (c) of the 31,944 main-lane object echoes: stdout/stderr/interrupted/isImage/noOutputExpected 31,944 each, backgroundTaskId 2,593, backgroundCwdHint 1,052, staleReadFileStateHint 969, gitOperation 708, persistedOutputPath 235, persistedOutputSize 235, returnCodeInterpretation 159, dangerouslyDisableSandbox 66, timedOutAfterMs 31, assistantAutoBackgrounded 21 (all false, all beside a backgroundTaskId, versions 2.1.156 x2 / 2.1.159 x6 / 2.1.177 x13). Code site src/model/record.rs lines 292-298 matches the claim's snippet verbatim.",
"rule": "(b) one tally per LINE containing the quoted key name, over every *.jsonl under ~/.claude/projects (a key occurring twice on one line counts once); (c) one tally per KEY PRESENT in the `toolUseResult` object of a main-lane Bash echo, denominator 31,944 echoes.",
"note": "A partial refutation landed. The claim's `none of those five occurs on any record in the corpus` is true only under its own stated counting rule (`over every top-level transcript`); widening the same census to the subagent lanes finds `backgroundEndsWithFinalResponse` on 8 records. The claim's headline point survives and is strengthened: the key set really is open at any one version, and the drift runs in BOTH directions AND across lanes, so a surface enumerating it from one version's top-level transcripts would be wrong twice over. The `assistantAutoBackgrounded` half is exact - 21 records, the three named versions, always false beside a backgroundTaskId, and the only one of the nine names checked that scores 0 in the 2.1.258 binary (`backgroundCwdHint` 4, `gitOperation` 6, `returnCodeInterpretation` 8, `structuredContent` 24, `backgroundEndsWithFinalResponse` 6, `backgroundedByTurnAbort` 8, `backgroundedToDeliverMessage` 10, `backgroundedByUser` 14 matching lines). Two further live keys the claim does not name are worth adding to any future enumeration: `backgroundCwdHint` (1,204 corpus lines) and `gitOperation` (827)."
}
]
},
{
"id": "BASH-027",
"area": "bash-shell",
"behavior": "A FOREGROUND Bash command that outruns its timeout is not killed: Claude Code moves it to the background, mints a `backgroundTaskId`, records the elapsed limit as `toolUseResult.timedOutAfterMs` (its schema description reads `Set when the command hit its timeout and was auto-backgrounded; the timeout value in ms`), returns `interrupted:false` with EMPTY `stdout` and `stderr`, and writes the tool_result text `Command did not complete within its <N>s timeout and was moved to the background (ID: <id>). Output is being written to: <path>`. Measured on 31 such results: all 31 carry a `backgroundTaskId`, all 31 have empty stdout and stderr, none is marked interrupted.",
"depends": "csift's `status` BACKGROUND scan mints a shell task only from a `tool_use` whose `input.run_in_background` is true, and its raw-byte candidate needles cover `Command running in background` but not the moved-to-background wording, so a timeout-promoted task is missing from the very section that exists to name still-running work. `recover` is hit differently: the promoted call's echo carries empty stdout, so a bash READ content anchor can never gate on it - the command's output exists only in the harness task output file.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "25-33",
"snippet": " [\n &b\"run_in_background\"[..],\n b\"Command running in background\",\n b\"async_launched\",\n b\"task-notification\",\n b\"stopped by the user\",\n b\"\\\"Monitor\\\"\",\n b\"Monitor started\",\n ]"
}
],
"instrument": "`strings` over the installed Claude Code binary (2.1.258) for the optional schema member `timedOutAfterMs` with its description and for the literal `was moved to the background (ID: `. Corpus: for every Bash echo carrying `timedOutAfterMs`, read `interrupted`, `backgroundTaskId`, `stdout`, `stderr` and the tool_result block's text. Counting rule: one tally per Bash echo carrying the key, over every top-level transcript. Measured 2026-09-02: 31 results, 31 with a `backgroundTaskId`, 31 with empty stdout and stderr, 0 interrupted, 31 whose text carries the moved-to-background sentence.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "measured 2026-09-02"
},
"first_seen_claude_code": "2.1.210",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'timedOutAfterMs:[A-Za-z()]*\\.optional\\(\\)\\.describe\\(\"[^\"]*\"' and | rg -o '.{0,120}was moved to the background \\(ID: .{0,140}'; (b) python3 over ~/.claude/projects: for every top-level Bash echo whose `toolUseResult` carries `timedOutAfterMs`, read `interrupted`, `backgroundTaskId`, `stdout`, `stderr` and the tool_result block's text; (c) controlled A/B fixture under `--claude-home <tmpdir>`: two synthetic single-turn sessions, identical except that one Bash `tool_use` sets `run_in_background:true` and the other sets nothing, both answered by a `toolUseResult` carrying a `backgroundTaskId`; ran `csift status @11111111 --claude-home <tmpdir>` and `csift status @22222222 --claude-home <tmpdir>`.",
"observed": "(a) schema member verbatim: `timedOutAfterMs:A().optional().describe(\"Set when the command hit its timeout and was auto-backgrounded; the timeout value in ms\")`; the text template verbatim: `Command did not complete within its ${Math.max(1,Math.round(d/1000))}s timeout and was moved to the background (ID: ${e}). Output is being written to: ${n}.`, one arm of a four-way selector whose other arms are `Command was manually backgrounded by user with ID: `, `Command was moved to the background (ID: ...) so that a message that arrived while it was running can reach you; it was not interrupted.` and the default `Command running in background with ID: `. (b) 31 echoes carry `timedOutAfterMs`; 31 of 31 carry a `backgroundTaskId`; 31 of 31 have empty stdout AND empty stderr; 0 are `interrupted:true`; 31 of 31 have the moved-to-background sentence in the tool_result text; 0 of 31 came from a `tool_use` that set `run_in_background`. Rendered second counts: 120s x20, 180s x1, 300s x4, 420s x1, 600s x5. (c) the explicit fixture prints `verdict idle-background-open`, `background 1 open; 0 completed...` and a `bg shell bfxexplicit` row; the timeout-promoted fixture prints `verdict idle-eot` with NO background section and no bg row at all.",
"rule": "(b) one tally per Bash `tool_result` block whose carrying record's `toolUseResult` object contains the key `timedOutAfterMs`, over the 67 top-level transcripts; stdout/stderr counted empty when the field is absent or the empty string. (c) one status invocation per fixture session; a task is 'entered the BACKGROUND section' iff it appears as a `bg` row in text or in the JSON verdict's `background.tasks[]`.",
"note": "Every number and every quoted string reproduced exactly. The dependency is also confirmed, and by a stronger instrument than the ledger cites: the A/B fixture isolates `input.run_in_background` as the single differing byte and shows csift calling the timeout-promoted session `idle-eot` - truly stopped - while a shell of its own is still running in the harness background. On a real session carrying a 2.1.257 timeout promotion, `csift status --format json` likewise returned that promotion's task id in no `background.tasks[]` row (the id surfaces only inside the `last` excerpt, as the completion `<task-notification>`). One incidental repo observation: the doc comment above the needle array in src/live/background_scan.rs calls it `The five raw-byte needles` while the array holds seven; the claim's snippet at lines 25-33 is verbatim."
}
]
},
{
"id": "BASH-028",
"area": "bash-shell",
"behavior": "The foreground Bash timeout is the call's own `input.timeout` clamped to a ceiling of 600,000 ms, defaulting to 120,000 ms when the call names none (the binary carries the pair as adjacent constants `120000` and `600000`). Both bounds are overridable per environment by `BASH_DEFAULT_TIMEOUT_MS` and `BASH_MAX_TIMEOUT_MS`, both on the settings `env` allowlist, and on a main-agent lane that can auto-background `CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS` lowers the effective limit further to `min(requested, max(env, 2000))`.",
"depends": "csift never predicts how long a lane may legitimately sit at an unreturned Bash `tool_use`: `agents` reports `pending_classification: awaiting-execution` and hands the caller `pending_since_utc` to weigh. The ceiling is what makes that weighing possible on a lane where auto-backgrounding applies - past ten minutes the harness's own timeout path would have returned a background promotion, so continued silence points at a blocked or abandoned lane rather than a slow command.",
"code": [
{
"path": "src/cli/agents_args.rs",
"lines": "107-110",
"snippet": " STALENESS: `pending_classification: awaiting-execution` means slow OR wedged OR \\\n abandoned; jsonl cannot tell them apart, and at corpus scale a lane pending for \\\n hours/days is overwhelmingly \\\"parent session ended, nobody is coming back\\\", not \\\n in-flight work: weigh `pending_since_utc` against now yourself; \\"
}
],
"instrument": "`strings` over the installed Claude Code binary (2.1.258) for the adjacent constant pair `120000,` `600000` used by the two timeout resolvers (`BASH_DEFAULT_TIMEOUT_MS` returns the env value when positive else the 120000 constant; `BASH_MAX_TIMEOUT_MS` returns `Math.max(env, default)` else `Math.max(600000, default)`), and for the auto-background clamp `Math.min(requested, Math.max(env, 2000))` gated on `CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS`. Corpus: join each Bash echo carrying `timedOutAfterMs` to its `tool_use` in the same transcript and tabulate `(input.timeout, timedOutAfterMs)`. Counting rule: one pair per timed-out call over every top-level transcript. Measured 2026-09-02, 31 pairs: no timeout named -> 120000 (20 calls); 180000 -> 180000; 300000 -> 300000 (4); 420000 -> 420000; 600000 -> 600000; 900000 -> 600000 (3); 2400000 -> 600000.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02"
},
"first_seen_claude_code": "2.1.210",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var Ivo=2000;function xBt\\(.{0,500}' and | rg -o '.{0,220}BASH_DEFAULT_TIMEOUT_MS.{0,220}' and | rg -o '.{0,400}liedMs:Ne\\}\\)'; (b) python3 over ~/.claude/projects: join each top-level Bash echo carrying `timedOutAfterMs` to its `tool_use` in the same transcript and tabulate (input.timeout, timedOutAfterMs).",
"observed": "(a) the resolvers verbatim: `var Rvo=120000,Pvo=600000;function DTe(e=process.env){let n=e.BASH_DEFAULT_TIMEOUT_MS;if(n){let r=nl(n);if(!isNaN(r)&&r>0)return r}return Rvo}function Jze(e=process.env){let n=e.BASH_MAX_TIMEOUT_MS;if(n){let r=nl(n);if(!isNaN(r)&&r>0)return Math.max(r,DTe(e))}return Math.max(Pvo,DTe(e))}`; the clamp itself verbatim: `let{command:we,description:Ie,timeout:Pe,run_in_background:De}=e,Ne=Math.min(Pe||nme(),sZ(),r?.maxTimeoutMs??1/0);if(Pe&&Ne<Pe)o?.({key:\"timeout_clamped\",requestedMs:Pe,appliedMs:Ne})` (nme = the 120000 default resolver, sZ = the 600000 max resolver); the auto-background clamp verbatim: `var Ivo=2000;function xBt({requestedTimeoutMs:e,isMainAgent:n,canAutoBackground:r,env:o=process.env}){if(!n||!r)return e;let d=o.CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS;if(!d)return e;let f=nl(d);if(isNaN(f)||f<=0)return e;return Math.min(e,Math.max(f,Ivo))}`; the tool schema renders the ceiling live as `timeout:$M(A().optional()).describe(`Optional timeout in milliseconds (max ${sZ()})`)`; both `BASH_DEFAULT_TIMEOUT_MS` and `BASH_MAX_TIMEOUT_MS` sit in the quoted name Set consulted by the settings-`env` allowlist predicate, whose caller does `for(let{key:e,value:o}of n.values())if(aUe(e,o))process.env[e]=o` after `filterSettingsEnv`. (b) 31 pairs: no timeout named -> 120000 (20 calls); 180000 -> 180000 (1); 300000 -> 300000 (4); 420000 -> 420000 (1); 600000 -> 600000 (1); 900000 -> 600000 (3); 2400000 -> 600000 (1).",
"rule": "One (input.timeout, timedOutAfterMs) pair per Bash echo carrying `timedOutAfterMs`, joined by tool_use id inside the same transcript, over the 67 top-level transcripts; every one of the 31 resolved in-file.",
"note": "Holds on both instruments, and the binary supplies the mechanism the ledger inferred from the pairs: the clamp is literally `Math.min(input.timeout || default(120000), max(600000), ...)`, and it emits a `timeout_clamped` telemetry key carrying requestedMs and appliedMs. Two details worth folding into a future revision rather than corrections, since nothing in the claim is wrong: the min has a THIRD term `r?.maxTimeoutMs ?? Infinity` (a per-caller ceiling that can cut below 600000), and `Jze` returns `Math.max(600000, default)`, so raising BASH_DEFAULT_TIMEOUT_MS above 600000 raises the ceiling with it rather than producing a default above the max. `CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS` is read from the process env but is NOT in the settings-env allowlist Set that carries the two BASH_* names."
}
]
},
{
"id": "BASH-029",
"area": "bash-shell",
"behavior": "A `backgroundTaskId` on a Bash echo does not mean the call asked to be backgrounded: 2.1.258 distinguishes FIVE background triggers in its own telemetry - user, turn_abort, deliver_message, timeout and explicit - selected from `backgroundedByUser`, `backgroundedByTurnAbort`, `backgroundedToDeliverMessage`, `timedOutAfterMs`, else explicit. Measured over 2,593 backgrounded Bash results, 60 (2.31%) came from a `tool_use` that never set `run_in_background`: 31 carry `timedOutAfterMs`, 21 carry the retired `assistantAutoBackgrounded:false`, and 8 name no trigger at all while still rendering the ordinary `Command running in background with ID:` text.",
"depends": "csift's background scan keys a shell LAUNCH on `input.run_in_background == true` and only then joins a carrier to it by tool_use id, so those 60 results never enter the BACKGROUND section: `status` under-reports off-turn shell work by exactly the non-explicit trigger classes, and silently. The id join itself is sound; the launch predicate is the narrow part.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "49-59",
"snippet": " Block::ToolUse {\n id: Some(id),\n name: Some(name),\n input: Some(input),\n ..\n } if (matches!(name.as_str(), \"Bash\" | \"PowerShell\")\n && input\n .get(\"run_in_background\")\n .and_then(serde_json::Value::as_bool)\n == Some(true))\n || name == \"Monitor\" =>"
}
],
"instrument": "`strings` over the installed Claude Code binary (2.1.258) for the background-acknowledgement telemetry event, whose trigger expression maps `backgroundedByUser` -> user, `backgroundedByTurnAbort` -> turn_abort, `backgroundedToDeliverMessage` -> deliver_message, `timedOutAfterMs` -> timeout, else explicit. Corpus: for every Bash echo carrying a `backgroundTaskId`, join the result block's `tool_use_id` back to its `tool_use` in the same transcript and read `input.run_in_background`. Counting rule: one tally per backgrounded Bash echo, over every top-level transcript; every launch resolved in-file. Measured 2026-09-02: 2,593 backgrounded results, 2,533 with `run_in_background:true`, 60 without (31 `timedOutAfterMs`, 21 `assistantAutoBackgrounded`, 8 with neither).",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "measured 2026-09-02"
},
"first_seen_claude_code": "2.1.156",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'tengu_bash_task_ack.{0,400}'; (b) python3 over ~/.claude/projects: for every top-level Bash echo carrying a `backgroundTaskId`, join the result block's tool_use_id back to its `tool_use` in the same transcript, read `input.run_in_background`, and for the non-explicit ones bucket by trigger and by the leading sentence of the tool_result text.",
"observed": "(a) the trigger expression verbatim: `tengu_bash_task_ack\",{trigger:Bn.backgroundedByUser?b(\"user\"):Bn.backgroundedByTurnAbort?b(\"turn_abort\"):Bn.backgroundedToDeliverMessage?b(\"deliver_message\"):Bn.timedOutAfterMs!==void 0?b(\"timeout\"):b(\"explicit\"),ends_with_final_response:Bn.backgroundEndsWithFinalResponse===!0,shell:b(\"bash\")}` - with an identical PowerShell twin ending `shell:b(\"powershell\")`. (b) 2,593 backgrounded Bash echoes; 2,533 from a `tool_use` with `run_in_background:true`; 60 (2.31%) without - and none of those 60 set the field to false, all 60 omit it entirely. Trigger split of the 60: 31 carry `timedOutAfterMs`, 21 carry `assistantAutoBackgrounded:false`, 8 carry neither. Text split: the 21 and the 8 both render `Command running in background with ID`, the 31 render `Command did not complete within its <N>s timeout and was moved to the ...`. The 60 span 17 versions from 2.1.156 to 2.1.257.",
"rule": "One tally per Bash `tool_result` block whose carrying record's `toolUseResult` object has a non-null `backgroundTaskId`, over the 67 top-level transcripts; the launch is the `tool_use` with the matching id in the same file (all 60 resolved in-file); text bucket = the substring of the tool_result text before its first colon.",
"note": "Exact on every number: 2,593 / 2,533 / 60, and 31 / 21 / 8. The binary gives the five triggers verbatim in the order the claim states, and adds that the same five-way selector is duplicated for the PowerShell tool, so the narrow-launch-predicate consequence applies to `PowerShell` records identically. The dependency is confirmed by construction and by the fixture used for BASH-027: csift's `ingest_launches` can only mint a task from a `tool_use` whose `input.run_in_background` is true (or a Monitor), so a result whose launch omits the field enters no BACKGROUND row no matter which of the four non-explicit triggers produced it - and the 8 no-trigger cases are the sharpest form, since their text is byte-identical to an explicit launch's yet csift still mints nothing."
}
]
},
{
"id": "BASH-030",
"area": "bash-shell",
"behavior": "A Bash call can opt out of Claude Code's sandbox per call, and the decision is recorded on BOTH sides of the call: the `tool_use` carries `input.dangerouslyDisableSandbox` and the result echoes the same boolean back under the same name. Measured on 66 echoes carrying the key - 63 `true` and 3 `false` - the launching `tool_use` carried the identical value in all 66.",
"depends": "csift models neither side: the lexical layers (the dangerous-rm escalation prediction and the operand attribution that reads `input.command`) run identically on a sandboxed and an unsandboxed command, and `--count-by tool` reports one `Bash` key regardless - so a call's recorded sandbox posture reaches a consumer only through the raw line (`csift show --raw`).",
"code": [
{
"path": "src/bash_mutations/entry.rs",
"lines": "5-15",
"snippet": "/// One heuristically-detected Bash file mutation. `verb` is from the fixed allowlist\n/// below (it is the lexical command/operator that touched the path), and `path` is the\n/// operand exactly as it appeared (quote-stripped, otherwise verbatim).\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct BashMutation {\n pub path: String,\n pub verb: &'static str,\n /// The shell cwd in effect at this operand's segment (see [`cwd`] for the\n /// tracked-cwd mechanism and the resolution classes built on it).\n pub cwd_at: CwdAt,\n}"
}
],
"instrument": "`strings` over the installed Claude Code binary (2.1.258) for `dangerouslyDisableSandbox` (26 matching output lines, including the result-mapper member). Corpus: for every Bash echo carrying the key, join the result block's `tool_use_id` back to its `tool_use` and compare the echoed boolean with `input.dangerouslyDisableSandbox`. Counting rule: one tally per Bash echo carrying the key, over every top-level transcript. Measured 2026-09-02: 66 echoes, 63 true / 3 false, input value equal to the echoed value in 66 of 66.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02"
},
"first_seen_claude_code": "2.1.210",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'dangerouslyDisableSandbox' and | rg -o 'noOutputExpected:Lqo\\(e\\.command\\).{0,600}'; (b) python3 over ~/.claude/projects: for every top-level Bash echo carrying `dangerouslyDisableSandbox`, join the result block's tool_use_id back to its `tool_use` and compare the echoed boolean with `input.dangerouslyDisableSandbox`; (c) rg -n -i 'dangerouslyDisableSandbox|useSandbox' src/ in the csift repo.",
"observed": "(a) 26 matching output lines, among them the constructor member that performs the copy verbatim: `dangerouslyDisableSandbox:\"dangerouslyDisableSandbox\" in e ? e.dangerouslyDisableSandbox : void 0` - the echo is written from the tool INPUT by an `in` test, which is why a `false` is echoed rather than dropped. (b) 66 echoes carry the key, 63 true and 3 false; the launching `tool_use` carried the identical value in 66 of 66; the 66 span versions 2.1.210 (2), 2.1.211 (36), 2.1.218 (1), 2.1.219 (1), 2.1.231 (2), 2.1.233 (24). (c) zero hits - the string appears nowhere in csift's src/. Code site src/bash_mutations/entry.rs lines 5-15 matches the claim's snippet verbatim.",
"rule": "One tally per Bash `tool_result` block whose carrying record's `toolUseResult` object contains the key `dangerouslyDisableSandbox`, over the 67 top-level transcripts; equality compared as JSON booleans against the joined `tool_use`'s `input.dangerouslyDisableSandbox`.",
"note": "Exact: 66 echoes, 63 true / 3 false, input equal to echo in 66 of 66. The binary explains the shape the corpus shows - the copy is guarded by `\"dangerouslyDisableSandbox\" in e`, so the key is present on the echo exactly when the call named it, at either value, and absent otherwise; that is what makes the two sides agree 66 of 66 by construction rather than by coincidence. The dependency also holds under a direct grep: csift models neither side, so the recorded sandbox posture is reachable only through the raw line."
}
]
},
{
"id": "CLS-001",
"area": "classification",
"behavior": "The label taxonomy Claude Code's record shapes require is exactly 33 leaf classes across three roles (user, agent, harness), and `agent.thinking.narration` is the first leaf whose dotted path another leaf (`agent.thinking`) prefixes.",
"depends": "`Class::ALL` is the single source of truth behind `-t` selector derivation, the clap value parser, the `--help` leaf list, the census keys and the zero-match probe, and a drift-guard unit test pins its length at 33; the prefix relationship forces dot-SEGMENT prefix selection rather than plain string prefixing, so `-t agent.thinking` selects both leaves and pure reasoning needs `-T agent.thinking.narration`. A record shape added without its leaf is unreachable; a leaf added without updating the pinned count fails the gate.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "203-208",
"snippet": " pub const ALL: &'static [Class] = &[\n Class::UserMessage,\n Class::UserAnswer,\n Class::UserRejection,\n Class::UserUnsent,\n Class::UserQueued,"
}
],
"instrument": "`csift search '' -t nosuchleaf` errors listing every valid selector - count the listed leaves, and compare with the `Class::ALL.len()` assertion in the drift-guard unit test. Counting rule: one per entry of `Class::ALL`.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "SPEC.md section 5; AGENTS.md section 3.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search '' -t nosuchleaf ; csift search '' -t agent.thinking --count-by label <csift-project-dir> ; csift search '' -t agent.thinking.narration --count-by label <csift-project-dir> ; sed -n '193,228p' src/model/taxonomy.rs ; cargo test --bin csift class",
"observed": "The clap value-parser error lists 44 selectors, of which 33 are full dotted leaf paths: 5 under user, 5 under agent plus 3 under agent.communication, 20 under harness. src/model/taxonomy.rs lines 193-226 hold a Class::ALL array with exactly those 33 entries, and src/model/tests/classify_support.rs:83 carries assert_eq!(n, 33, \"Class::ALL leaf count drifted\"); the model::tests suite runs green. Segment-prefix selection measured: -t agent.thinking returned 2 census keys, agent.thinking 10192 + agent.thinking.narration 888 = 11080 records; -t agent.thinking.narration returned 1 key, 888 records.",
"rule": "One leaf per entry of Class::ALL. A selector printed by the error counts as a leaf only if it is a full dotted path present in Class::ALL; bare roles and mid-path prefixes (user, agent, agent.tool, harness.meta, ...) are excluded. Census records are counted once per surviving leaf.",
"note": "agent.thinking is the only leaf in Class::ALL whose dotted path is a segment-prefix of another leaf, and the two-key census under -t agent.thinking is the direct proof that selection is dot-segment rather than plain string prefixing. Code site verified verbatim at src/model/taxonomy.rs:193-198."
}
]
},
{
"id": "CLS-002",
"area": "classification",
"behavior": "`isMeta` is an AUTHORSHIP flag and `isVisibleInTranscriptOnly` a summary DISPLAY flag; neither reports whether the model received the content. The only measured visibility instrument is the presence of a `message{}` field: zero of 24 non-record line types carry one, and every `user`/`assistant` record does.",
"depends": "csift's bare-role selectors expand to LLM-visible leaves using `Class::llm_visible`, keyed on the `message{}` instrument rather than on `isMeta`, `isVisibleInTranscriptOnly`, parentUuid threading or `compactMetadata.preservedMessages` membership; misreading `isMeta` as visibility poisoned a real last-human-touch hook.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "167-170",
"snippet": " /// all - pure compaction metrics. (The compaction SUMMARY is visible: the\n /// DAG threads through it; `isVisibleInTranscriptOnly` is a display flag on\n /// summaries, not a delivery flag, and `isMeta` is an authorship flag -\n /// neither is a visibility instrument.)"
},
{
"path": "src/model/taxonomy.rs",
"lines": "172-174",
"snippet": " /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n /// over every non-record line type in the corpus; every user/assistant record"
}
],
"instrument": "`csift stats <target> --format json | tail -1 | jq .line_types` enumerates the file's line types, then `csift show <target> --line <n> --raw | jq 'has(\"message\")'` on one line of each type. Counting rule: one probe per distinct top-level `type`.",
"located": {
"claude_code": null,
"csift": "0.9.4",
"source": "CHANGELOG.md 0.9.4 and 0.10.0; src/model/taxonomy.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift stats --format json (line_types summed over every row) ; a python probe reading every line of 26 top-level transcripts and counting isinstance(record.get('message'), dict) per top-level type ; a byte-regex census of \"subtype\" on system lines across all 64 top-level transcripts",
"observed": "16 distinct top-level line types corpus-wide: attachment 932244, assistant 855488, user 454942, last-prompt 58394, permission-mode 58276, mode 58098, ai-title 57440, queue-operation 26984, system 25362, agent-name 24546, file-history-snapshot 8102, file-history-delta 3926, atis-latch 1522, bridge-session 1384, fork-context-ref 66, cost-state 30. 11 distinct system subtypes: stop_hook_summary 5635, turn_duration 4623, away_summary 1628, scheduled_task_fire 459, compact_boundary 229, model_refusal_fallback 51, api_error 29, local_command 15, agents_killed 10, model_refusal_no_fallback 2, informational 1. Over the 26 probed transcripts (chosen to cover all 14 non-record top-level types, including the four rarest): every non-record type scored 0 records carrying a message{} object; assistant scored 17793/17793 and user 9269/9269.",
"rule": "Non-record line types = the 13 non-record top-level types other than system, plus the 11 system subtypes = 24. One message{}-presence probe per distinct type/subtype; presence means the top-level message field deserializes to a JSON object.",
"note": "The claim's number 24 reproduces exactly under that expansion rule. Without expanding system into its subtypes the corpus shows 14 non-record top-level types, so the rule has to travel with the number. The instrument the claim proposes (csift show --raw plus jq has(\"message\")) works but is one probe per type; the python pass gives the same answer with full coverage instead of a single sampled line. Code sites verified verbatim at src/model/taxonomy.rs:159-162 and 164-166."
}
]
},
{
"id": "CLS-003",
"area": "classification",
"behavior": "Claude Code injects background/automation completions as a type:\"user\", non-isMeta (the key is absent entirely), STRING-content record wrapped in <task-notification>...</task-notification>. Only <task-id> and <summary> are universal (100.0% of 2797 measured); <tool-use-id>, <status> and <output-file> ride together on 70% (every non-monitor kind), <event> on 29.9% (monitor kinds only) and <result> on 11.6%. It passes every genuine-user gate, so on disk it looks exactly like a human turn.",
"depends": "csift lets the pulse OPEN a turn but reparents it to `harness.notification.<kind>` and renders `Record::automation_label()` as `[<kind> <task-id> <status>] <summary>` instead of dumping the raw XML wrapper; hardcoding the literal `workflow` for every trigger mislabeled 81% of them on one captured session (85 background-command + 2 agent pulses). A pulse carrying a `<result>` is a background-agent REPORT, so it additionally carries `agent.communication.inbox` (child to self).",
"code": [
{
"path": "src/model/markers.rs",
"lines": "120-127",
"snippet": "/// Prefix of a `<task-notification>…</task-notification>` user record - a MACHINE-INJECTED\n/// automation trigger (a background-command / workflow / spawned-task completion notice CC\n/// inserts as a `type:\"user\"`, non-`isMeta`, STRING-content record). It LOOKS like a human\n/// turn to [`Record::is_genuine_user`] (it passes every gate), so it DOES open a turn - but\n/// it is an automation pulse, not the operator's prose. [`Record::automation_trigger`]\n/// classifies it so surfaces can LABEL the segment (`[workflow <id> completed] <summary>`)\n/// instead of dumping the raw `<task-id>`/`<output-file>`/`<status>` XML wrapper.\npub const TASK_NOTIFICATION_PREFIX: &str = \"<task-notification>\";"
}
],
"instrument": "csift search '' -t harness.notification --count-by label, cross-checked against a raw census of records whose origin.kind == \"task-notification\". Do NOT use csift search '<task-notification>' --count-by label: a notification-classified record's matchable text is the rendered automation label, not the raw XML, so that query returns 423 records corpus-wide and ZERO of them under harness.notification.* - they are all prose mentions of the tag in other records.",
"located": {
"claude_code": null,
"csift": "0.2.0",
"source": "SPEC.md section 5.1; AGENTS.md section 3.3; src/model/automation.rs comment; src/model/markers.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python over all 64 top-level transcripts selecting records whose origin.kind == \"task-notification\" and whose message.content is a string starting with <task-notification>, counting inner-tag presence ; csift search '<task-notification>' --count-by label ; csift search '' -t harness.notification --count-by label ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}(tool-use-id|output-file).{0,60}'",
"observed": "2807 records carry origin.kind == task-notification; 2797 of them open with the XML wrapper. Inner-tag presence over those 2797: <task-id> 2797 (100.0%), <summary> 2797 (100.0%), <tool-use-id> 1957 (70.0%), <status> 1962 (70.1%), <output-file> 1957 (70.0%), <event> 835 (29.9%), <result> 325 (11.6%). Shape is uniform at 2807/2807: type user, message.role user, message.content a STRING, userType external, isSidechain false, and no isMeta key present. The 2.1.258 binary carries the tag constants verbatim: 's7t=\"fork-source\",_p=\"task-notification\",Use=\"task-id\",jdr=\"tool-use-id\",qBe=\"task-type\"' and 'Gdr=\"output-file\",zx=\"status\",j0=\"summary\"'. Label check: csift search '' -t harness.notification --count-by label returns 2810 hits (background-command 1519, monitor 946, subagent 198, workflow 142, task 5), and a separate probe shows 49/49 <result>-bearing pulses in one project dir additionally carry agent.communication.inbox while 131/131 pulses without <result> carry only their notification leaf.",
"rule": "One observation per RECORD whose parsed origin.kind == \"task-notification\" and whose message.content is a string; a tag counts as present if its literal open form occurs anywhere in the content. Label counts are per surviving leaf, one hit per section.",
"note": "The record shape holds exactly as claimed and the <result> to inbox coupling reproduces. Two corrections: the claim treats <status> as one of the always-present inner tags (measured 70.1%) and <output-file> as optional (measured 70.0%, i.e. the same population as <status>), and the claim's stated instrument inverts - it counts prose mentions and finds zero real pulses. The 81% mislabel figure is historical and cannot be re-measured against the current classifier. Code site verified verbatim at src/model/markers.rs:115-122."
}
]
},
{
"id": "CLS-004",
"area": "classification",
"behavior": "A <task-notification> <summary> opens with a fixed leading classifier and that leading phrase, never the quoted command name, is the class: a Background command \"...\" pulse is always a background command even when its quoted description contains the words monitor or re-arm (measured: 644 such pulses corpus-wide, all background-command). The classifier set in Claude Code 2.1.258 is larger than the four csift names explicitly - at least Background command \"...\", Dynamic workflow \"...\", Agent \"...\", Monitor event: \"...\", Remote task \"...\", Remote task blocked on input: ..., Cloud review failed: ..., Stopped by a worker restart: ..., The container running this session was restarted before background work reported back: ..., and a consolidated N memory file... form - and every head csift does not name falls through to the task slug.",
"depends": "`AutomationKind::from_summary` matches the leading phrase case-insensitively into the five slugs `background-command`/`workflow`/`agent`/`monitor`/`task` (anything unrecognized falls back to `task`), so `search -t harness.notification.monitor` reports the real Monitor tool's pulses only; the retired quoted-name heuristic double-booked, producing 40 `monitor` records against zero genuine Monitor pulses on one measured project. The classifier does not cover `ScheduleWakeup` wakeup-tick prompts, which are `isMeta` records that never reach it.",
"code": [
{
"path": "src/model/automation.rs",
"lines": "33-40",
"snippet": " /// Classify from the `<summary>`. Case-insensitive on the known leading prefixes; anything\n /// else (or a missing summary) is [`AutomationKind::Task`]. The `monitor`/`scheduled`/`cron`\n /// LEADING prefixes route a Monitor-tool pulse or termination notice\n /// (`Monitor event: …` / `Monitor \"…\" …`) to [`AutomationKind::Monitor`]. A `Background\n /// command \"…\"` pulse is ALWAYS `background-command`, whatever its quoted name says\n /// (v0.10.0: the former quoted-name heuristic - `monitor`/`re-arm`/`liveness` in the name\n /// routed to `Monitor` - predates the real Monitor tool and double-booked; measured on one\n /// project it produced 40 `monitor` records against zero genuine Monitor pulses). This"
},
{
"path": "src/model/automation.rs",
"lines": "44-63",
"snippet": " pub fn from_summary(summary: Option<&str>) -> Self {\n let s = summary.unwrap_or(\"\").trim_start();\n // The classifiers are a fixed leading phrase; match the longest-distinguishing\n // prefix case-insensitively so a `Background command \"…\"` is not mistaken for `task`.\n let lower = s.to_ascii_lowercase();\n if lower.starts_with(\"background command\") {\n AutomationKind::BackgroundCommand\n } else if lower.starts_with(\"dynamic workflow\") || lower.starts_with(\"workflow\") {\n AutomationKind::Workflow\n } else if lower.starts_with(\"monitor\")\n || lower.starts_with(\"scheduled\")\n || lower.starts_with(\"cron\")\n {\n AutomationKind::Monitor\n } else if lower.starts_with(\"agent\") {\n AutomationKind::Agent\n } else {\n AutomationKind::Task\n }\n }"
},
{
"path": "src/model/automation.rs",
"lines": "67-75",
"snippet": " pub fn slug(self) -> &'static str {\n match self {\n AutomationKind::BackgroundCommand => \"background-command\",\n AutomationKind::Workflow => \"workflow\",\n AutomationKind::Agent => \"agent\",\n AutomationKind::Monitor => \"monitor\",\n AutomationKind::Task => \"task\",\n }\n }"
}
],
"instrument": "`csift search '' <project-dir> --count-by label | rg 'harness.notification'` cross-checked against a raw census of summary heads, `rg -o '<summary>[^<]*' <transcript> | cut -c1-30 | sort | uniq -c`. Counting rule: one record per pulse, bucketed by the leading summary phrase; a project whose summaries never open with Monitor/Scheduled/cron must report zero monitor records.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md section 5.1; SPEC.md section 6 v0.10.0 ledger item 4b; AGENTS.md section 3.3; CHANGELOG.md 0.10.0; src/model/automation.rs comment; SPEC.md v0.10.0 ledger; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' -t harness.notification.background-command --format json, filtering rendered excerpts whose quoted name matches /monitor|re-arm|rearm|liveness/i ; a raw python census of the 2797 XML-wrapped pulses bucketed by the leading phrase of <summary> ; csift search '' -t harness.notification --count-by label ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'summary:`[^`]{0,90}'",
"observed": "644 background-command pulses corpus-wide carry monitor / re-arm / liveness inside their quoted command name and every one classifies background-command, not monitor (top specimens are 53, 27, 20, 19 and 16 records of re-arm-named ticks). Independent raw census by leading summary phrase: background-command 1517, monitor 946, agent 187, workflow 142, task 5 (total 2797); csift's own leaf census over the same corpus: background-command 1519, monitor 946, subagent 198, workflow 142, task 5 (total 2810) - the +11 on the subagent leaf is the 10 plain-text agents-stopped notices plus one, and the leading-phrase buckets otherwise agree record-for-record. The 2.1.258 binary carries the classifier constants: 'Gdr=\"output-file\",zx=\"status\",j0=\"summary\",Lke=\"Background command \",Htt=\\'Agent \"\\',mIn=\"finished\",qdr=`\" ${mIn}`,mkt=\\'Remote task \"\\'' plus the templates summary:`Dynamic workflow \"${l}\" completed` / `Dynamic workflow \"${l}\" failed: ${Y}` and summary:`Monitor event: \"${Bt(e)}\"`.",
"rule": "One record per pulse, bucketed by the leading phrase of its <summary> matched case-insensitively; a pulse counts as quoted-name-contaminated if /monitor|re-arm|rearm|liveness/i matches inside the quotes that follow the classifier.",
"note": "The mechanism holds and is now measured at 644 records rather than the 40 the code comment cites for one project. The refinement is that the enumeration of leading classifiers is not exhaustive against 2.1.258: the binary carries at least six further summary heads that csift's from_summary absorbs into its task fallback, and a mIn=\"finished\" / qdr=`\" ${mIn}` pair meaning a summary can also read Background command \"...\" finished. Nothing misroutes today - the fallback is correct - but the ledger wording should say the four named classifiers plus a task fallback rather than implying the four are the whole set. Code sites verified verbatim at src/model/automation.rs:33-40, 44-63 and 67-75."
}
]
},
{
"id": "CLS-005",
"area": "classification",
"behavior": "A Monitor / cron cadence pulse carries its real outcome in an `<event>` payload (`STAGE2_OUTPUT_READY`, `[Monitor timed out - re-arm if needed.]`) and usually carries NO `<status>` tag at all, unlike a completion pulse.",
"depends": "csift's `automation_label` fills the status slot in the fallback order `<status>`, then `<event>`, then the literal `completed`, so a monitor pulse renders the event it actually carries instead of a fabricated outcome the record does not hold.",
"code": [
{
"path": "src/model/automation.rs",
"lines": "93-97",
"snippet": " /// The `<event>` payload, if present - where a Monitor / ScheduleWakeup pulse carries its\n /// real outcome (`STAGE2_OUTPUT_READY`, `[Monitor timed out - re-arm if needed.]`). Often\n /// the only outcome signal on a Monitor pulse (which usually has no `<status>`), so the\n /// label falls back to it instead of fabricating `completed`.\n pub event: Option<String>,"
}
],
"instrument": "`grep -o '<event>[^<]*' ~/.claude/projects/*/*.jsonl | sort | uniq -c` for the event census, then `csift search '' @<session> -t harness.notification.monitor --max-count 3` and read the rendered label: the status slot must show the event payload, not `completed`. Counting rule: one rendered label per pulse record.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.0",
"source": "SPEC.md section 5.1; AGENTS.md section 3.3; src/model/automation.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "a raw python census over the 2797 XML-wrapped pulses tabulating (kind by leading summary phrase, '<status>' in content, '<event>' in content) ; csift search '' -t harness.notification.monitor --max-count 8 (reading the rendered label) ; csift search '' -t harness.notification --format json parsing the status slot out of each rendered label ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Monitor timed out[^\"]{0,40}'",
"observed": "Of 946 monitor pulses corpus-wide, 835 (88.3%) carry an <event> and NO <status>, and 111 (11.7%) carry a <status> and no <event>. <event> occurs on ZERO of the 1851 non-monitor pulses (background-command 1517, agent 187, workflow 142, task 5 - all <status>, no <event>). Rendered labels put the event payload in the status slot: '[monitor <id> [Monitor timed out \\u2014 re-arm if needed.]] Monitor event: \"...\"' and '[monitor <id> STEP=[113/124...]'; the rendered census counts exactly 111 monitor records whose slot reads 'completed', matching the 111 <status>-bearing pulses one for one. The 2.1.258 binary carries the literal 'Monitor timed out \\u2014 re-arm if needed.]'.",
"rule": "One rendered label per pulse record; a pulse is monitor-kind if its <summary> opens case-insensitively with monitor, scheduled or cron. Slot equals 'completed' is counted only on an exact match of that word.",
"note": "The fallback order never fabricates: the count of monitor pulses rendering 'completed' (111) equals the count that actually carry a <status> tag (111), so the remaining 835 render the <event> they really hold. The doc comment's example payload STAGE2_OUTPUT_READY was not observed in this corpus; the event payloads seen are step/progress strings, terminal markers and the timed-out sentinel - the example is illustrative, not a claim. Code site verified verbatim at src/model/automation.rs:93-97."
}
]
},
{
"id": "CLS-006",
"area": "classification",
"behavior": "Claude Code CONCATENATES several harness sections into ONE `type:\"user\"` record: a batched record can hold multiple `<task-notification>` pulses and/or several inbound peer sections of mixed kind (prose, `idle_notification`, `teammate_terminated`), each closed by `</task-notification>`, `</teammate-message>` or `</agent-message>`, and a peer tag can also be QUOTED inside a notification's `<result>` span.",
"depends": "csift recognizes a section only at a BOUNDARY (content start, immediately after the relay preamble, or right after a prior section's close tag), scans ALL sections and UNIONs their labels with notification precedence over a peer tag quoted inside a notification span, mirrors that masking exactly in the per-section text render so classification and render never drift, and emits one hit per section; a plain `contains` check mislabels any genuine message that merely quotes a tag mid-prose - common in csift's own development sessions.",
"code": [
{
"path": "src/model/peer.rs",
"lines": "120",
"snippet": "pub(crate) fn is_section_boundary(prefix: &str) -> bool {"
},
{
"path": "src/model/classify.rs",
"lines": "242-245",
"snippet": " // BATCHED mixed-family sections: a `<task-notification>` automation pulse and/or an\n // inbound peer message can be concatenated in ONE record. Scan ALL sections and UNION\n // their labels, with notification precedence over a peer tag quoted inside a\n // notification span (P1c M3). When ≥1 section matches, that fully classifies the record."
}
],
"instrument": "`csift search '<task-notification>' -t user.message -c` must be far smaller than `rg -c '<task-notification>' <same scope>` (rg counts raw occurrences, csift counts boundary-anchored sections), and a record with two notifications must yield two hits under `csift search '' -t harness.notification --format json`. Counting rule: one record, N sections, labels unioned.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 5.2; src/model/classify.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python over one project dir's 23 top-level transcripts counting, per string-content record, the number of </task-notification> + </teammate-message> + </agent-message> closes ; rg -o --no-filename '<task-notification>' *.jsonl | wc -l and rg -c '<task-notification>' *.jsonl in that same dir ; csift search '<task-notification>' -t user.message -c ; csift show @<id> --line <n> --format json on one 8-section record",
"observed": "253 string-content records carry at least one section close; 319 sections total; 22 records carry 2 or more, histogram {2:6, 3:6, 4:2, 5:4, 7:2, 8:2}. Close-tag census: </task-notification> 181, </teammate-message> 135, </agent-message> 3. Mixed payload kinds observed inside one record: prose, {\"type\":\"idle_notification\"...} and {\"type\":\"teammate_terminated\"...}, all behind the relay preamble 'Another Claude session sent a message: '. A <task-notification> open tag was observed INSIDE a <teammate-message> span (tag sequence teammate-message, /teammate-message, teammate-message, task-notification, /teammate-message). Boundary anchoring: rg reports 1557 lines and 2043 raw occurrences of the open tag in that one project dir, while csift reports exactly 1 user.message record mentioning it corpus-wide. The 8-section record emits 8 units, every unit carrying the unioned label set ['agent.communication.signal','agent.communication.inbox'], split 5 signal / 3 inbox.",
"rule": "One record, N sections, labels unioned: sections counted by close tags in message.content; emitted units counted as JSON rows whose line equals the record's line.",
"note": "Every part of the claim reproduced, including the quoted-tag-inside-a-notification case that a plain contains check would mislabel. The 1557-vs-1 gap is the size of the error boundary anchoring avoids, and the surviving 1 user.message hit is the correct outcome - a genuine message that quotes the tag mid-prose. Code sites verified verbatim at src/model/peer.rs:120-127 and src/model/classify.rs:242-245."
}
]
},
{
"id": "CLS-007",
"area": "classification",
"behavior": "Every member of the stopped/orphan notice family shares one record shape: type:\"user\", userType:\"external\", isSidechain:false, origin:{\"kind\":\"task-notification\"}, NO isMeta key at all, and a message.content that is a STRING (never an array). promptSource is \"system\" on the large majority but is not universal - of 2807 measured records it is \"system\" on 2685 and absent on 122. Of those 2807, 2797 were XML-wrapped and 10 were plain text.",
"depends": "csift classifies these by CONTENT shape (the `<task-notification>` prefix, or the agents-stopped templates) rather than by `origin`/`promptSource`, so a plain-text notice with no recognizable template still classifies as the human; `origin.kind` and `promptSource` are the authoritative discriminator csift does not yet read.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "325-329",
"snippet": " // The harness's agents-stopped notice (v0.10.0): a kill notice, not the human.\n if is_agents_stopped_notice(s) {\n push_unique(out, Class::NotificationSubagent);\n return;\n }"
}
],
"instrument": "`rg -l '\"origin\":\\{\"kind\":\"task-notification\"\\}' ~/.claude/projects --glob '*.jsonl'` then, for each matching record, test whether `message.content.lstrip().startswith('<task-notification>')`. Counting rule: one observation per RECORD whose parsed `origin.kind == \"task-notification\"`.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python over all 64 top-level transcripts selecting records whose parsed origin.kind == \"task-notification\", tabulating (type, userType, isSidechain, promptSource, whether the isMeta key exists, isMeta value) and whether message.content lstrip-starts with the XML wrapper ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'promptSource|\"kind\":\"task-notification\"|task-notification\"\\}'",
"observed": "2807 records carry origin.kind == task-notification (the claim says 2700). 2797 are XML-wrapped and 10 are plain text (the claim says 2683 and 17). message.content is a python str on 2807/2807 - never an array. type is user, userType is external, isSidechain is false and the isMeta key is ABSENT on 2807/2807. promptSource is NOT uniform: \"system\" on 2685 records (95.7%) and the key is absent on 122 (4.3%). All 10 plain-text specimens are agents-stopped notices, 9 plural and 1 singular. The binary carries origin:{kind:\"task-notification\",source:...} and mode:\"task-notification\" at the enqueue sites, and 33 occurrences of promptSource.",
"rule": "One observation per RECORD whose parsed origin.kind == \"task-notification\"; XML-wrapped means message.content.lstrip() starts with '<task-notification>'; a field is 'present' only when the key exists in the record object.",
"note": "The shape claim holds on five of its six fields at 100%, and the wrapped/plain split reproduces at the same order (99.6% wrapped here vs 99.4% claimed). Two corrections: the totals have moved (2807/2797/10 rather than 2700/2683/17 - the corpus grew) and promptSource is a 95.7% field, not a universal one, so it is a weaker discriminator than the claim's depends paragraph implies while origin.kind is exact at 2807/2807. Code site verified verbatim at src/model/classify.rs:325-329."
}
]
},
{
"id": "CLS-008",
"area": "classification",
"behavior": "When background agents are killed from the UI, Claude Code writes a plain-STRING type:\"user\" notice in one of two templates - the singular Background agent \"<desc>\" was stopped by the user. or the plural N background agents were stopped by the user: \"<desc>\", \"<desc>\". - naming a count and truncated prompt prefixes but NEVER an agent id (measured 0 of 10 records carry a >=16-hex token), and it is delivered through enqueuePendingNotification so it triggers no generation (no assistant record follows any of the 10).",
"depends": "csift treats the notice as a synthetic marker so it is excluded from `is_genuine_user` and never opens a turn, classifies it `harness.notification.subagent`, and renders `[subagent stopped] <notice>`; without the exclusion each kill inflates the human-turn count (9 corpus specimens were previously read as `user.message`). Because the notice names no id, `csift status` can report that agents were stopped but not WHICH, and the raw needle `stopped by the user` doubles as the verifiable synth marker guarding the whole-file prefilter gate.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "60-66",
"snippet": "pub fn is_agents_stopped_notice(content: &str) -> bool {\n let s = content.trim_start();\n // The singular template names the agent: `Background agent \"<desc>\" was stopped by\n // the user.` (no count).\n if s.starts_with(\"Background agent \\\"\") && s.contains(\" was stopped by the user\") {\n return true;\n }"
},
{
"path": "src/model/markers.rs",
"lines": "72-79",
"snippet": " (rest.starts_with(\" background agent was stopped by the user\")\n || rest.starts_with(\" background agents were stopped by the user\"))\n && rest.contains(AGENTS_STOPPED_MARKER)\n}\n\n/// The raw-byte marker the agents-stopped notice always carries (the synth-marker\n/// needle for its fabricated `[subagent stopped]` label prefix).\npub const AGENTS_STOPPED_MARKER: &str = \"stopped by the user\";"
},
{
"path": "src/live/background_scan.rs",
"lines": "195-197",
"snippet": " let note = format!(\n \"{count} background agent(s) were stopped by the user at {} - the notice names \\\n no id, so csift cannot mark which agents it stopped\","
}
],
"instrument": "csift search 'stopped by the user' --count-by label, asserting the user.message key is absent and that the harness.notification.subagent count equals the on-disk notice count. Do NOT assert that every matched record lands under harness.notification.subagent: the phrase also occurs in ordinary prose, and corpus-wide the query returns 242 records across 9 labels of which only 10 are notices (agent.tool.result 122, agent.tool.use 60, agent.thinking 29, agent.communication.inbox 10, harness.notification.subagent 10, agent.message 8, agent.communication.sent 4, harness.compaction.summary 2, harness.meta.attachment 1).",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md section 5.1; SPEC.md section 6 v0.10.0 ledger item 4a; AGENTS.md section 3.3a; src/model/markers.rs comment; CHANGELOG.md 0.10.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,140}background agents? (was|were) stopped by the user.{0,120}' ; python over all 64 top-level transcripts matching either notice template at content start, testing each for a >=16-hex token and recording the type of the next record in the file ; csift search 'stopped by the user' --count-by label ; rg -c 'stopped by the user' on one project dir's top-level transcripts",
"observed": "The binary carries the generator verbatim: 'let Fo=kn.length===1?`Background agent \"${kn[0]}\" was stopped by the user.`:`${kn.length} background agents were stopped by the user: ${kn.map((jn)=>`\"${jn}\"`).join(\", \")}.`;return Ye.enqueuePendingNotification({agentId:Ze(),value:Fo,mode:\"task-notification\"' - both templates exact, the map is over descriptions (kn is built from Ko.description) so no id can appear, and enqueuePendingNotification is the delivery path. On disk: 10 notice records corpus-wide, 0 of them containing a >=16-hex token, and the record following a notice is never an assistant record (file-history-snapshot 3, user 3, attachment 5). csift labels all 10 harness.notification.subagent and 0 user.message. Scoped rg on one project dir reports 76 lines containing the phrase.",
"rule": "A record counts as a notice only if its trimmed content starts with 'Background agent \"' and contains ' was stopped by the user', or matches ^\\d+ background agents? (were|was) stopped by the user. rg counts raw lines; csift counts records whose content matches at content start.",
"note": "The behavior is confirmed at the binary level, which is stronger evidence than the corpus alone. Corrections: the specimen count is now 10 rather than 9, and the claim's instrument overstates - the phrase appears in prose 232 times in this corpus, so the assertion that survives is 'zero under user.message' plus 'notice count equals subagent-leaf count', not 'every record'. Two adversarial findings worth recording: a loose contains-based probe finds an 11th candidate that is a compaction summary quoting the phrase, and csift's stricter start-anchored template correctly rejects it; and the binary carries a SEPARATE template `Task \"${e.description}\" (${e.taskId}) was stopped by the user.` written with isMeta:!0 which DOES name an id and which is_agents_stopped_notice does not match (harmless today because isMeta records are excluded anyway, but it means 'the notice never names an id' is true only of the Background agent family). Code sites verified verbatim at src/model/markers.rs:55-61 and 67-74 and src/live/background_scan.rs:205-207."
}
]
},
{
"id": "CLS-009",
"area": "classification",
"behavior": "An asynchronous background Agent spawn's tool_result is a LAUNCH ACK, not the child's report: its text opens 'Async agent launched successfully' and it carries structured toolUseResult.{isAsync:true, status:\"async_launched\"} under the spawn's own tool_use_id (272 records corpus-wide, both signals on all of them); the child's actual report arrives LATER as a <task-notification> pulse carrying a <result>. The structured status:\"async_launched\" marker is broader than this family - a further 174 records are background WORKFLOW launches carrying that status with no isAsync key and the different ack text 'Workflow launched in background. Task ID: <id>'.",
"depends": "csift prefers the structured signal and falls back to the text prefix (`Record::is_async_launch_ack`), labeling the ack `agent.tool.result` ONLY and excluding it from subagent-return detection, while the later `<result>`-bearing pulse is what additionally carries `agent.communication.inbox` with direction child to self; conflating them attributes the child's report to the launch instant.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "180-185",
"snippet": "/// The leading sentence of an ASYNC/background `Agent` spawn's launch-confirmation tool_result\n/// (`\"Async agent launched successfully.\\nagentId: …\"`). This is a launch ACK, NOT the child's\n/// report - the report arrives LATER via the `<task-notification>` `<result>` pulse (G1 → inbox).\n/// On disk the ack also carries the structured `toolUseResult.{isAsync:true, status:\"async_launched\"}`\n/// shape ([`Record::is_async_launch_ack`] prefers the structured signal, falls back to this prefix).\npub const ASYNC_LAUNCH_ACK_PREFIX: &str = \"Async agent launched successfully\";"
},
{
"path": "src/model/classify.rs",
"lines": "81-88",
"snippet": " pub(crate) fn is_async_launch_ack(&self) -> bool {\n if let Some(probe) = self.tur_probe() {\n if probe.status.as_ref().and_then(serde_json::Value::as_str) == Some(\"async_launched\")\n || probe.is_async.as_ref().and_then(serde_json::Value::as_bool) == Some(true)\n {\n return true;\n }\n }"
}
],
"instrument": "`jq -r 'select(.toolUseResult.status==\"async_launched\") | .toolUseResult.agentId' <file> | wc -l` versus `grep -c 'Async agent launched successfully' <file>`, then `csift search 'Async agent launched' --format json | jq -r '.label'` must be `agent.tool.result` on every row and never `agent.communication.inbox`. Counting rule: one ack per launch, one inbox record per `<result>` pulse.",
"located": {
"claude_code": null,
"csift": "0.4.0",
"source": "SPEC.md section 5.2; AGENTS.md section 3.3; src/model/markers.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python over all 64 top-level transcripts counting records with toolUseResult.status == \"async_launched\" or toolUseResult.isAsync == true versus records whose tool_result text opens 'Async agent launched successfully', tabulating the toolUseResult key sets ; csift show --line <list> --format json over every such record in one project dir, censusing the labels array ; a paired census of pulses with and without <result> in that same dir ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Async agent launched successfully'",
"observed": "446 records corpus-wide carry the structured signal. 272 of them carry BOTH isAsync:true and a tool_result text opening 'Async agent launched successfully' (co-occurrence 272/272, key set agentId/canReadOutputFile/description/isAsync/outputFile/prompt/resolvedModel/status). The other 174 carry status:\"async_launched\" with NO isAsync key, a different key set (runId/scriptPath/status/summary/taskId/taskType/transcriptDir/workflowName) and a different ack text opening 'Workflow launched in background. Task ID: <id>'. All 86 Agent-spawn acks in one project dir label exactly ['agent.tool.result'] - never agent.communication.inbox. In that same dir 49 pulses carry a <result> and 49/49 additionally carry agent.communication.inbox (94 rows subagent+inbox, 4 rows workflow+inbox = 49 records at 2 label views each), while 131/131 pulses without a <result> carry only harness.notification.background-command. The binary carries the literal 'Async agent launched successfully'.",
"rule": "One ack per launch: a record counts as an Agent-spawn ack if toolUseResult.isAsync is true; as a workflow launch if status is async_launched and isAsync is absent. One inbox record per <result>-bearing pulse. csift show emits one JSON row per label view, so a dual-labeled record yields two rows - divide by the label count to get records.",
"note": "The classification behavior is confirmed at 86/86 acks labelling agent.tool.result only, and the <result>-pulse coupling at 49/49 versus 131/131. The refinement is scope: the claim describes status:\"async_launched\" as the Agent ack's structured signature, but it covers two ack families (446 records = 272 Agent + 174 workflow). csift's is_async_launch_ack ORs the two signals so it catches both, which is wider than the claim's wording describes and is not itself a bug - it just means the marker cannot be read as an Agent-spawn discriminator. Code sites verified verbatim at src/model/markers.rs:167-172 and src/model/classify.rs:81-88."
}
]
},
{
"id": "CLS-010",
"area": "classification",
"behavior": "A `SendMessage` tool_use and a `Task`/`Agent`/`Workflow` spawn tool_use are COMMUNICATIONS between sessions, not merely tool calls: the spawn addresses a child, and `SendMessage` addresses a named peer.",
"depends": "csift dual-labels them agent.tool.use plus agent.communication.{sent,signal} and emits the record ONCE under the richer communication view, with a from / to direction. The direction source differs by tool: a SendMessage reads input.to, while a spawn addresses its child by the spawn target's name or id and a Workflow spawn has no target to name yet, so 169 of 1819 sent hits (9.3%) render to as '?'. The owner's own id renders as the literal 'self'.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "214-216",
"snippet": " Class::CommInbox,\n Class::CommSent,\n Class::CommSignal,"
}
],
"instrument": "`csift search '' -t agent.communication --count-by label --format json @<a team session>`, cross-checked with `rg -o '\"name\":\"SendMessage\".{0,120}' <transcript> | rg -o '\"type\":\"[a-z_]+\"'`. Counting rule: one per matched record, deduped to the richest label.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 5.2"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' -t agent.communication --count-by label ; csift search '' -t agent.communication --format json censusing (label, tool_name), the labels array and the from/to pair ; csift search '' -t agent.tool.use --count-by tool ; csift show @<id> --line <n> --format json on one SendMessage record ; a python read of that record's raw tool_use block",
"observed": "Corpus-wide the communication leaves hold 10804 records (inbox 8644, sent 1819, signal 400). 1818 records carry the label SET ('agent.tool.use','agent.communication.sent') and emit under the comm view. Tool split for sent: Agent 916, SendMessage 725, Workflow 178; signal from SendMessage 79. Under -t agent.tool.use --count-by tool, SendMessage counts 804 = 725 + 79 exactly, so every SendMessage tool_use is dual-labeled. Dedup measured on one record: csift show returns exactly 1 row, label agent.communication.sent, labels ['agent.tool.use','agent.communication.sent']. Direction fields are the hit keys from and to, not a nested object: the raw block's input keys are ['content','message','recipient','to','type'] and its input.to value is rendered verbatim as the hit's to while from renders the literal 'self'. Inbox hits render <peer> to self (top peers 3512, 976, 631, 544, 433 records; a named lead 428). to-shape by spawning tool: Agent 835 name-embedded child id / 79 bare name / 1 bare hex / 1 unknown, SendMessage 589 bare name / 136 name-embedded id, Workflow 168 unknown / 10 bare name.",
"rule": "One per matched record, deduped to the richest surviving label; census keys pass the active -t filter. A record's label set is read from the labels array, so a dual-labeled record contributes one record and two labels.",
"note": "The behavior claim reproduces exactly, including the once-only emission and the self literal. The single correction is the direction provenance: input.to is the source for SendMessage only (725 sent + 79 signal), while the 916 Agent and 178 Workflow spawns take their to from the spawn target, and every Workflow spawn but ten renders '?' because the child id does not exist at spawn time. Code site verified verbatim at src/model/taxonomy.rs:204-206."
}
]
},
{
"id": "CLS-011",
"area": "classification",
"behavior": "A `<teammate-message ...>` body that is a JSON object `{\"type\":\"<sig>\"}` is a control SIGNAL - observed types are `idle_notification` (334), `task_assignment` (49), `shutdown_request` (36), `teammate_terminated` (25) and `shutdown_approved` (25) - while a prose body is a plain message; a `SendMessage` whose `input.type` (or nested `message.type`) is anything but `message`/`direct` is likewise a signal (observed outbound: `shutdown_request` 38, `shutdown_response` 41). Every prose `SendMessage` observed carried the explicit `input.type == \"message\"`; the `direct` value and an absent `type` are tolerated by the predicate but were never seen on disk.",
"depends": "`agent.communication.signal` versus `agent.communication.inbox` splits on exactly this, so a shutdown request is not counted as peer prose.",
"code": [
{
"path": "src/model/peer.rs",
"lines": "13-15",
"snippet": " /// The control-signal type when the body is `{\"type\":\"<sig>\"}` (idle_notification /\n /// shutdown_request / teammate_terminated / shutdown_approved / …); `None` for prose.\n pub signal_type: Option<String>,"
},
{
"path": "src/model/peer.rs",
"lines": "269-272",
"snippet": "pub(crate) fn send_message_is_signal(input: Option<&serde_json::Value>) -> bool {\n let Some(input) = input else {\n return false;\n };"
}
],
"instrument": "`csift search '' -t agent.communication.signal --format json | jq -r .excerpt | rg -o '\"type\":\"[a-z_]+\"' | sort | uniq -c`. Counting rule: one record per signal-bodied section.",
"located": {
"claude_code": null,
"csift": null,
"source": "src/model/peer.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'idle_notification|shutdown_request|teammate_terminated|shutdown_approved|shutdown_response' | sort | uniq -c AND csift search '' -t agent.communication.signal --raw | python3 (pull the JSON body of every <teammate-message>/<agent-message> section and every SendMessage tool_use input.type) AND the same census on -t agent.communication.sent",
"observed": "Binary 2.1.258: idle_notification x6, shutdown_request x18, shutdown_response x16, shutdown_approved x7, teammate_terminated x5. Corpus: 394 records classify agent.communication.signal. Inbound signal-bodied sections: idle_notification 334, task_assignment 49, shutdown_request 36, teammate_terminated 25, shutdown_approved 25. Outbound SendMessage input.type: shutdown_request 38, shutdown_response 41. Records with neither shape: 0. The prose leaf agent.communication.sent carries 711 SendMessage tool_use blocks and input.type is the literal \"message\" on all 711 (0 \"direct\", 0 absent).",
"rule": "One count per signal-bodied <teammate-message>/<agent-message> section and per SendMessage tool_use block, taken over the records csift classifies agent.communication.signal; the split is checked by running the identical census over agent.communication.sent.",
"note": "Two signal types the claim omits are live and not rare: task_assignment (49 records) and shutdown_response (41 blocks, outbound only). The split predicate itself is clean - 711/711 prose sends carry type \"message\" and 0 signal-bodied records fell through to the prose leaf."
}
]
},
{
"id": "CLS-012",
"area": "classification",
"behavior": "The tools that spawn a subagent are named exactly `Task`, `Agent` and `Workflow` - all three registered in 2.1.258, though only `Agent` (897 blocks) and `Workflow` (177) were exercised in this corpus and `Task` appears 0 times. A peer message is sent with `SendMessage`, whose recipient rides `input.to`; `input.recipient` is not an alternative but a duplicate alias - both keys were present and byte-equal on all 790 observed blocks. A named/teammate spawn carries `input.name` (291 of 897 Agent blocks) and a typed spawn `input.subagent_type` (853 of 897); a `Workflow` spawn carries `name`/`script`/`scriptPath` and never `subagent_type`.",
"depends": "Communication direction (self to child, self to the named recipient) and the whole topology join key off these literal names; a rename breaks `agents`, `search -t agent.communication.sent`, and the teammate name-join.",
"code": [
{
"path": "src/model/peer.rs",
"lines": "262-264",
"snippet": "pub(crate) fn is_spawn_tool_name(name: &str) -> bool {\n matches!(name, \"Task\" | \"Agent\" | \"Workflow\")\n}"
},
{
"path": "src/model/peer.rs",
"lines": "304-306",
"snippet": "pub(crate) fn spawn_target_name(input: Option<&serde_json::Value>) -> Option<String> {\n let input = input?;\n for key in [\"name\", \"subagent_type\"] {"
}
],
"instrument": "`csift search '' @<session> --count-by tool` lists every tool name seen. Counting rule: one count per tool_use block, keyed by `name`.",
"located": {
"claude_code": null,
"csift": null,
"source": "src/model/peer.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\"Bash\",\"BashOutput\"[^\\]]{0,600}' | sort -u AND rg -o 'var yt=\"Agent\",bnr=\"Launch a new agent to handle complex, multi-step tasks\"' AND csift search '' -t agent.communication --raw | python3 (count tool_use names, spawn input keys, SendMessage recipient keys)",
"observed": "Binary tool registry array contains, adjacent: \"Agent\",\"Task\",\"Workflow\",\"Skill\", and \"SendMessage\" later in the same array. Verbatim: `var yt=\"Agent\",bnr=\"Launch a new agent to handle complex, multi-step tasks\"`; `o_=\"Task\"` sits beside `hj=\"fork\"` and `Enr=\"agent:builtin:fork\"`. Corpus tool_use blocks on the agent.communication leaves: Agent 897, Workflow 177, SendMessage 790, Task 0. Agent input keys: prompt 897, description 897, subagent_type 853, model 616, name 291, run_in_background 266. Workflow input keys: script 135, args 55, description 40, scriptPath 32, name 10, resumeFromRunId 10, run_in_background 2 - no subagent_type. SendMessage: `to` present on 790/790 AND `recipient` present on 790/790, and the two values are equal on all 790.",
"rule": "One count per tool_use block keyed by `name`, and one count per input key present on that block, over every record csift classifies under the agent.communication role prefix.",
"note": "The `input.recipient` fallback never fired alone - it is a redundant alias on every block, so a reader keyed only on `to` loses nothing today. `Task` is registered but dormant in this corpus, so csift's three-name matcher is a superset, not dead code."
}
]
},
{
"id": "CLS-013",
"area": "classification",
"behavior": "Claude Code injects a fixed continuation marker as an `isMeta` `type:\"user\"` record whose content is exactly `Continue from where you left off.` when resuming a session. Current measurement (CC 2.1.258, whole corpus, csift 0.10.0): 6 records classify `harness.schedule.continuation`; 470 records contain the phrase anywhere, the rest being prose that discusses the marker.",
"depends": "csift classifies it `harness.schedule.continuation` and excludes it from `is_genuine_user`, so a resumed session's human-turn count is not inflated by one per resume and resume points stay enumerable; without the exact-content rule the record falls through the isMeta gate unlabeled.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "187-190",
"snippet": "/// The fixed harness-injected continuation marker (GOLD §5) - `harness.schedule.continuation`.\n/// A `type:\"user\"` (`isMeta`) record CC injects to resume a session from where it left off.\n/// Verified across real `~/.claude/projects` data (522 occurrences), exact content.\npub const SCHEDULE_CONTINUATION_MARKER: &str = \"Continue from where you left off.\";"
}
],
"instrument": "csift search '' -t harness.schedule.continuation --count-by label --format json | tail -1 (matched_records = the classified set), then csift search '' -t harness.schedule.continuation --raw and confirm every record is type=user / isMeta=true / content exactly the marker.",
"located": {
"claude_code": "2.1.191",
"csift": "0.4.0",
"source": "AGENTS.md section 3.3a; src/model/markers.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Continue from where you left off\\.?' | sort | uniq -c AND csift search '' -t harness.schedule.continuation --count-by label --format json | tail -1 AND csift search '' -t harness.schedule.continuation --raw | python3 (print type/isMeta/role/content per record) AND csift search 'Continue from where you left off' --count-by label --format json | tail -1",
"observed": "Binary 2.1.258 carries the exact string 6 times. Corpus: matched_records 6 under harness.schedule.continuation; all 6 are type=user, isMeta=True, role=user, and message.content is a single text block whose text is exactly 'Continue from where you left off.'. The same phrase appears anywhere in 470 records overall (335 agent.tool.result, 65 agent.tool.use, 23 agent.message, ...), i.e. 464 of them are prose mentions.",
"rule": "Records, not lines: matched_records from the --count-by label summary. The classified figure counts records whose reconstructed content STARTS with the marker (csift's `trim_start().starts_with`); the 470 figure counts records containing the phrase anywhere.",
"note": "The record SHAPE reproduces perfectly and the marker string is live in the 2.1.258 binary. Only the 522 count fails to reproduce under any counting rule I could construct - 6 classified records, 470 phrase-bearing records. The stale figure was almost certainly a raw substring occurrence count over a different corpus state, and it should not be carried forward without its rule."
}
]
},
{
"id": "CLS-014",
"area": "classification",
"behavior": "The harness distinguishes a FIRED autonomous-loop / `ScheduleWakeup` timer tick - an `isMeta` `type:\"user\"` record opening `# Autonomous loop check`, whose next line opens `You're being invoked on a timer while the user is away or occupied.` (straight ASCII apostrophe) - from the loop DRIVER ticks `# Autonomous loop tick (dynamic pacing)` and `Run the autonomous check using the loop instructions established earlier in this conversation.`. TWO loop sentinels exist in 2.1.258, not one: `<<autonomous-loop-dynamic>>` (always used by `ScheduleWakeup`) and `<<autonomous-loop>>` (CronCreate-based autonomous loops); the binary explicitly warns not to confuse them.",
"depends": "csift routes the fired-timer forms to `harness.schedule.wakeup` and the driver forms to `harness.meta.loop`, matching the wakeup arm FIRST so `check` never falls through to `tick`; conflating them merges the scheduler with the driver in every census and makes a monitoring transcript unreadable. A generic cron tick's injected prompt is operator-authored free text with no universal marker and is deliberately left unlabeled rather than mislabeled.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "204-211",
"snippet": "/// The header of the harness-injected FIRED autonomous-loop / `ScheduleWakeup` timer tick (P1c\n/// M2a / oracle D12) - `harness.schedule.wakeup`. When the timer FIRES, the harness injects an\n/// `isMeta` `type:\"user\"` record whose content opens `# Autonomous loop check\\n\\nYou're being\n/// invoked on a timer …`. DISTINCT from the `meta.loop` DRIVER ticks\n/// ([`AUTONOMOUS_LOOP_TICK_PREFIX`] = `# Autonomous loop tick` / [`AUTONOMOUS_CHECK_MARKER`]):\n/// `check` ≠ `tick`, so the two prefixes never collide. The wakeup arm is matched BEFORE the\n/// meta.loop arm in [`Record::classify`], so the fired tick routes to `schedule.wakeup`.\npub const SCHEDULE_WAKEUP_LOOP_CHECK_PREFIX: &str = \"# Autonomous loop check\";"
},
{
"path": "src/model/markers.rs",
"lines": "229-233",
"snippet": "/// `harness.meta.loop` markers (GOLD §2, edge-fixtures G2) - autonomous-loop drivers (distinct\n/// from the [`SCHEDULE_WAKEUP_MARKER`] sentinel, which stays `harness.schedule.wakeup`).\npub const AUTONOMOUS_LOOP_TICK_PREFIX: &str = \"# Autonomous loop tick\";\n/// See [`AUTONOMOUS_LOOP_TICK_PREFIX`] - matched anywhere (it can sit mid-prompt).\npub const AUTONOMOUS_CHECK_MARKER: &str = \"Run the autonomous check\";"
},
{
"path": "src/model/markers.rs",
"lines": "202",
"snippet": "pub const SCHEDULE_WAKEUP_MARKER: &str = \"<<autonomous-loop-dynamic>>\";"
},
{
"path": "src/model/classify.rs",
"lines": "297-303",
"snippet": " if s.contains(SCHEDULE_WAKEUP_MARKER)\n || s.starts_with(SCHEDULE_WAKEUP_LOOP_CHECK_PREFIX)\n || s.contains(SCHEDULE_WAKEUP_TIMER_MARKER)\n {\n push_unique(out, Class::ScheduleWakeup);\n return;\n }"
}
],
"instrument": "`rg -c \"You're being invoked on a timer\" ~/.claude/projects/*/*.jsonl` and `rg -c '# Autonomous loop tick' <same>`, then `csift search '' -t harness.schedule.wakeup -c` and `-t harness.meta.loop -c`. Counting rule: one per raw occurrence for rg, one per classified record for csift; the two csift counts must not overlap.",
"located": {
"claude_code": null,
"csift": "0.4.0",
"source": "SPEC.md section 5.1; AGENTS.md section 3.3a; src/model/markers.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -n -A4 '^# Autonomous loop check$' AND rg -o '.{0,100}# Autonomous loop tick.{0,180}' AND rg -o '.{0,140}<<autonomous-loop>>.{0,160}' AND csift search '' -t harness.schedule.wakeup --count-by label --format json | tail -1 AND csift search '' -t harness.meta.loop --count-by label --format json | tail -1 AND csift search '' -t harness.schedule.wakeup --raw",
"observed": "Binary: `# Autonomous loop check` appears twice, each immediately followed by a line opening `You're being invoked on a timer while the user is away or occupied.` (straight ASCII apostrophe, two prompt variants). The driver is `# Autonomous loop tick (dynamic pacing)`, built by a function returning a template that opens `# Autonomous loop tick`; `Run the autonomous check using the loop instructions established earlier in this conversation.` appears 5 times. Verbatim: `var wa=\"ScheduleWakeup\",zAe=\"<<autonomous-loop>>\",roe=\"<<autonomous-loop-dynamic>>\"`, with an adjacent in-binary note `(There is a similar ${\"<<autonomous-loop>>\"} sentinel for CronCreate-based autonomous loops; do not confuse the two - ${\"ScheduleWakeup\"} always uses the -dynamic variant.)`. Corpus: matched_records 1 for harness.schedule.wakeup and 0 for harness.meta.loop; the single wakeup record is type=user, isMeta absent, role=user, plain operator prose that merely quotes the sentinel mid-text.",
"rule": "Binary: one count per distinct string occurrence in `strings -n 6` output. Corpus: matched_records from each leaf's --count-by label summary; the two leaves must not overlap, and a true fired tick would be an isMeta record whose content STARTS with the header.",
"note": "Two problems the instruments surfaced. (1) csift models only the `-dynamic` sentinel, so a fired CronCreate autonomous-loop tick carrying `<<autonomous-loop>>` would classify unlabeled; the header and body-sentence arms would still catch it if the runtime resolves the sentinel to the standard prompt, but the sentinel arm alone would not. (2) The wakeup arm in src/model/classify.rs:297 uses `s.contains(SCHEDULE_WAKEUP_MARKER)` and is NOT gated on isMeta, so the sole corpus record under harness.schedule.wakeup is a false positive: a genuine non-meta operator prompt that quotes the sentinel loses its user.message label entirely. No true fired tick exists anywhere in this corpus, so only the binary decides the emission half of the claim."
}
]
},
{
"id": "CLS-015",
"area": "classification",
"behavior": "Arming a `ScheduleWakeup` is an ordinary tool CALL, while the FIRED tick is a separate harness-injected prompt; a custom-prompt tick lands as an `isMeta` record.",
"depends": "csift classifies the arming call `agent.tool.use` and only the fired tick `harness.schedule.wakeup`, so a search for scheduled work must look at both surfaces; expecting the arming call under `harness.schedule.*` returns a definitive empty.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "293-299",
"snippet": " // harness.schedule.wakeup: the FIRED autonomous-loop / ScheduleWakeup timer tick. Three\n // fixed markers (P1c M2a): the `<<autonomous-loop-dynamic>>` sentinel, the `# Autonomous\n // loop check` header, and the `You're being invoked on a timer` body sentence. Matched\n // BEFORE the meta.loop arm - `check` ≠ `tick`, so the loop-DRIVER prefix never collides.\n if s.contains(SCHEDULE_WAKEUP_MARKER)\n || s.starts_with(SCHEDULE_WAKEUP_LOOP_CHECK_PREFIX)\n || s.contains(SCHEDULE_WAKEUP_TIMER_MARKER)"
}
],
"instrument": "`csift search 'ScheduleWakeup' --count-by label` (arming records key under `agent.tool.use`) and `csift search \"You're being invoked on a timer\" --count-by label` (fired ticks key under `harness.schedule.wakeup`). Counting rule: one record per key.",
"located": {
"claude_code": null,
"csift": "0.2.0",
"source": "SKILL.md wrong-assumption row on ScheduleWakeup"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search 'ScheduleWakeup' --count-by label AND csift search 'ScheduleWakeup' --count-by tool AND csift search 'ScheduleWakeup' -t harness.schedule -c AND a python3 scan of ONE session jsonl pairing every ScheduleWakeup tool_use input.prompt against the content of every later type:\"user\" record in the same file",
"observed": "Corpus-wide, records matching 'ScheduleWakeup' key under agent.tool.use 794 / agent.tool.result 595 / agent.thinking 173 / agent.message 133 and ZERO under any harness.schedule.* leaf; `-t harness.schedule -c` and `-t harness.schedule.wakeup -c` both print 0. In one session file (123,044 records): 556 ScheduleWakeup tool_use blocks, all 556 with a non-empty input.prompt, 535 distinct prompt strings; 443 of those distinct prompts reappear VERBATIM as the content of a type:\"user\" record in the same file, across 456 such records, of which 456/456 have isMeta true. Only 2 of the 556 arming prompts were the `<<autonomous-loop-dynamic>>` sentinel; the rest were operator-authored free text.",
"rule": "Arming side: one count per tool_use block named ScheduleWakeup. Fired side: exact string equality between an arming block's input.prompt and a later user record's message.content, counted once per matching record; isMeta read off that record.",
"note": "The pairing is the strongest instrument in this batch: 456 of 456 fired ticks are isMeta true, and the arming calls never reach a harness.schedule.* leaf (definitive empty, exit 0). A separate probe confirms the second half of the depends clause - searching a fired custom tick's own text returns only the 22 assistant arming tool_use blocks that carry the same string, never the isMeta fired records themselves, because a custom-prompt tick carries no universal marker and classifies to no label at all."
}
]
},
{
"id": "CLS-016",
"area": "classification",
"behavior": "Hook-injected feedback reaches the transcript as `isMeta` `type:\"user\"` records in two shapes emitted by Claude Code 2.1.258 - content opening `Stop hook feedback:` (29 records) and a `<local-command-caveat>` wrapper (155 records) - plus a third, hook-authored shape: the edit-retry notice `The last Edit failed because the target file was modified`, which is NOT a string in the 2.1.258 binary and whose single corpus instance rides inside a `Stop hook feedback:` body. All three land as isMeta user records; the edit-retry marker is matched ANYWHERE precisely because it nests.",
"depends": "csift classifies all three `harness.meta.hook` rather than `user.message`, so a hook's own prose is never counted as an operator turn in `search -t user` or in `verbatim`; the edit-retry marker is matched ANYWHERE in the content rather than as a prefix, precisely because it nests inside a stop-hook body.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "218-224",
"snippet": "/// `harness.meta.hook` markers (GOLD §2, edge-fixtures G2) - hook-injected feedback, NOT the\n/// operator: a stop-hook feedback message, a `<local-command-caveat>` wrapper, or the\n/// edit-failed-retry notice CC injects when an Edit's target changed under it. (These are\n/// `isMeta` user records that would otherwise fall through to `user.message`.)\npub const STOP_HOOK_FEEDBACK_PREFIX: &str = \"Stop hook feedback:\";\n/// See [`STOP_HOOK_FEEDBACK_PREFIX`] - the `<local-command-caveat>…` hook wrapper.\npub const LOCAL_COMMAND_CAVEAT_PREFIX: &str = \"<local-command-caveat>\";"
},
{
"path": "src/model/markers.rs",
"lines": "227",
"snippet": "pub const EDIT_RETRY_MARKER: &str = \"The last Edit failed because the target file was modified\";"
}
],
"instrument": "`rg -c 'Stop hook feedback:' ~/.claude/projects/*/*.jsonl` then `csift search 'Stop hook feedback' -t harness.meta.hook -c`, and confirm `-t user.message -c` on the same pattern is 0. Counting rule: one per raw occurrence for rg, one per record matching any of the three markers for csift.",
"located": {
"claude_code": null,
"csift": "0.4.0",
"source": "SPEC.md section 5.1; AGENTS.md section 3.3a; src/model/markers.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Stop hook feedback:|local-command-caveat|The last Edit failed because the target file was modified' | sort | uniq -c AND rg -o '.{0,60}was modified.{0,60}' AND for each of the three markers: csift search '<marker>' -t harness.meta.hook --raw | python3 (count type/isMeta/role and print content heads) AND csift search '<marker>' --count-by label",
"observed": "Binary: `Stop hook feedback:` x3, `local-command-caveat` x2, and ZERO occurrences of `The last Edit failed` or `target file was modified`; the current freshness-rejection wording in 2.1.258 is instead `... was modified after it was handed to you, so Reads of it no longer count`. Corpus: the `Stop hook feedback:` shape gives 29 records, all (type=user, isMeta=True, role=user); `<local-command-caveat>` gives 155 records, all (user, True, user); the edit-retry sentence gives exactly 1 record, (user, True, user), and its content head is `Stop hook feedback:\\nThe last Edit failed because the target ...`. Neither the `Stop hook feedback:` nor the `<local-command-caveat>` census contains a `user.message` key. 191 records classify harness.meta.hook on a default scan corpus-wide.",
"rule": "One record per marker match, restricted to records csift classifies harness.meta.hook, with (type, isMeta, role) tallied per record; the user.message check is the absence of that key from the same pattern's --count-by label output.",
"note": "The nesting prediction is confirmed exactly - the one record carrying the edit-retry sentence has content starting with `Stop hook feedback:`. What does not survive is the attribution: 2.1.258 contains no such string, so this is hook text, not a CC-injected notice. Keeping the marker is still correct (it costs nothing and catches the nested form), but the code comment claiming CC injects it should be corrected. csift's current stale-read wording in that family is `was modified after it was handed to you, so Reads of it no longer count`."
}
]
},
{
"id": "CLS-017",
"area": "classification",
"behavior": "Claude Code writes an `isMeta` pseudo-record whose content is an image reference in one of TWO prefixes - `[Image: ` (with a leading `source: <src>` field when the source is known, giving `[Image: source: ...]`, plus optional original/displayed dimension text) and `[Image source: <src>]` (the form used when display dimensions are unknown). Both end with `]` and are single-line. The harness recognises either prefix and skips such records when deciding whether a record is a real user turn, so it is neither operator prose nor a modeled harness event.",
"depends": "csift classifies it EMPTY (no label at all) so it can never be mislabeled `user.message`; an unmodeled record yields an empty label vector rather than a crash, and the image bytes themselves are reached through `csift image`, not this record.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "320-324",
"snippet": " // isMeta \"[Image: source:…]\" pseudo-record (G2): EXCLUDED - emit no label rather than\n // mislabel it user.message.\n if s.starts_with(IMAGE_SOURCE_PREFIX) {\n return;\n }"
},
{
"path": "src/model/classify.rs",
"lines": "336-343",
"snippet": " // M2b ROOT FIX: a genuine `user.message` is NEVER isMeta. An isMeta record that matched\n // no marker above is a harness-injected pseudo-turn (a generic cron/monitor tick, a\n // novel hook wrapper), NOT the operator - emit NOTHING rather than mislabel it\n // `user.message` (the role-level isMeta gate `is_genuine_user` already applies). Only\n // genuine, non-isMeta unmarked prose is `user.message`.\n if !self.is_meta.unwrap_or(false) {\n push_unique(out, Class::UserMessage);\n }"
},
{
"path": "src/model/markers.rs",
"lines": "235-237",
"snippet": "/// An `isMeta` `[Image: source:…]` pseudo-record (GOLD §2, edge-fixtures G2) - EXCLUDED from\n/// the taxonomy entirely (classify yields no label), so it is never mislabeled `user.message`.\npub const IMAGE_SOURCE_PREFIX: &str = \"[Image: source:\";"
}
],
"instrument": "`rg -c '\\[Image: source:' ~/.claude/projects/*/*.jsonl` then `csift search 'Image: source' --format json | jq -r '.label' | sort -u` must not contain `user.message`, and `--count-by label` must show no key for those records. Counting rule: one per record whose content starts with the prefix.",
"located": {
"claude_code": null,
"csift": "0.4.0",
"source": "SPEC.md section 5.1; AGENTS.md section 3.3a; src/model/markers.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var J9t=\"\\[Image: \",wNe=\"\\[Image source: \"' AND rg -o '.{0,320}every\\(\\(r\\)=>r\\.type===\"text\"&&typeof r\\.text===\"string\"&&ZGr.{0,200}' AND csift search '\\[Image: source:' --raw | python3 (count records whose content STARTS with the prefix) AND csift search '^\\[Image' --raw | python3 (prefix + type/isMeta/role tally)",
"observed": "Binary verbatim: `var J9t=\"[Image: \",wNe=\"[Image source: \"`. The builder returns `${wNe}${i}]` when display dimensions are unknown, else `${J9t}${c.join(\", \")}]` where the first element is `source: ${i}` - so BOTH `[Image: source: ...]` and `[Image source: ...]` are constructed. The harness's own recogniser is `function ZGr(e){return(e.startsWith(J9t)||e.startsWith(wNe))&&e.endsWith(\"]\")&&!e.includes(\"\\n\")}`, used by `JGr` inside the branch `if(o.isMeta===!0){if(JGr(o))continue;return!1}` of the routine that decides whether a record is a real user turn. Corpus: 0 records whose content starts with `[Image: source:`; the 349 records whose content starts with `[Image` are all (type=user, isMeta=False, role=user) and use the unrelated `[Image #N]` pasted-image placeholder inside genuine human prompts.",
"rule": "One record per record whose reconstructed content STARTS with the prefix; the binary half is one count per distinct string in `strings -n 6` output plus the reading of the surrounding constructor and recogniser.",
"note": "The isMeta half of the claim is now confirmed structurally rather than by example: the branch `if(o.isMeta===!0){if(JGr(o))continue;return!1}` proves CC writes these as isMeta records and excludes them from its own user-turn test, which is exactly why csift must not label them user.message. No instance has ever landed in this corpus, so the exclusion cannot be observed on disk - and csift's single prefix is narrower than the harness's own two-prefix recogniser."
}
]
},
{
"id": "CLS-018",
"area": "classification",
"behavior": "Claude Code emits a `redacted_thinking` block type whose content is opaque, and it is not a distinct semantic class from thinking: 2.1.258 lists it beside `thinking` in its block-type allowlist and tests the two with a single predicate `function Hpe(e){return e.type===\"thinking\"||e.type===\"redacted_thinking\"}`. No such block exists anywhere in this corpus, so the folding can only be confirmed against the binary and csift's own unit fixture, not against live data.",
"depends": "csift folds `redacted_thinking` into `agent.thinking` and renders the literal `[redacted thinking]`; an unhandled block type would fall to the tolerant `Unknown` arm and vanish from every `-t agent.thinking` census.",
"code": [
{
"path": "src/model/record.rs",
"lines": "346",
"snippet": "/// A `redacted_thinking` block - encrypted/opaque reasoning CC emits in place of a visible"
},
{
"path": "src/search/types.rs",
"lines": "17",
"snippet": "pub(crate) const REDACTED_THINKING_PLACEHOLDER: &str = \"[redacted thinking]\";"
}
],
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,110}redacted_thinking.{0,110}' | sort -u (the deciding instrument), plus a scoped rg for '\"type\":\"redacted_thinking\"' to record that the corpus has no instance.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 5.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,110}redacted_thinking.{0,110}' | sort -u AND cd ~/.claude/projects && BIG=$(du -sk ./*/ | sort -rn | head -1 | cut -f2-); rg -c --no-filename -F '\"type\":\"redacted_thinking\"' \"$BIG\" -g '*.jsonl' AND csift search 'redacted' -t agent.thinking --format json | python3 (count hits whose excerpt is exactly the placeholder)",
"observed": "Binary 2.1.258 carries `redacted_thinking` 49 times, including a block-type allowlist reading `...redacted_thinking:!0,search_result:!0,server_tool_use:!0,text:!0,...,thinking:!0,...`, the predicate `function Hpe(e){return e.type===\"thinking\"||e.type===\"redacted_thinking\"}`, the opacity test `function Ope(e){if(e.type===\"redacted_thinking\")return!0;if(e.type===\"thinking\"&&\"signature\"in e...`, and a guard `e.message.content.every((n)=>n.type===\"thinking\"||n.type===\"redacted_thinking\")`. On disk, in the largest single project dir (2.0 GB, 5,624 jsonl files): 0 lines matching `\"type\":\"redacted_thinking\"`, 20 lines merely containing the word. csift found 139 agent.thinking hits for the pattern `redacted`, of which 0 had the excerpt `[redacted thinking]`.",
"rule": "Binary: one count per string occurrence, plus reading of each surrounding predicate. Corpus: one count per LINE containing the exact block-type key/value pair, scoped to the largest project dir; the csift check counts hits whose rendered excerpt is byte-equal to the placeholder.",
"note": "The binary settles the semantic half decisively - CC's own code treats thinking and redacted_thinking as one predicate. The claim's stated instrument cannot run here: zero blocks on disk. Note also that a real block is unreachable by the obvious search, because it renders as `[redacted thinking]` and the raw bytes say `redacted_thinking`; only a pattern that is a substring of BOTH (e.g. `redacted`) survives the prefilter and matches the render."
}
]
},
{
"id": "CLS-019",
"area": "classification",
"behavior": "A hook's injected context lands as a `type:\"attachment\"` record whose payload is `{\"type\":\"hook_additional_context\",\"content\":[...]}`, where `content` is a string ARRAY in real data (one element per injected block), not a bare string.",
"depends": "`Record::hook_additional_context_text` joins the array with a newline (tolerating a bare string) and classifies the record `harness.meta.hook`; `search --additional-context` is the only default-off gate that reaches it, so a bare-string-only reader would return nothing and make every hook-injected context invisible to search.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "336-344",
"snippet": " pub fn hook_additional_context_text(&self) -> Option<String> {\n if !self.is_type(\"attachment\") {\n return None;\n }\n let v = self.attachment_value()?;\n let att = v.as_object()?;\n if att.get(\"type\").and_then(serde_json::Value::as_str) != Some(\"hook_additional_context\") {\n return None;\n }"
},
{
"path": "src/search/scan.rs",
"lines": "350-351",
"snippet": " static HOOK_CONTEXT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memmem::Finder::new(b\"hook_additional_context\"));"
}
],
"instrument": "`csift search '<a phrase your hook injects>' @<session> --additional-context` must hit while the same query without the flag returns a definitive absence; then confirm the array shape with `rg -oNI '\"type\":\"hook_additional_context\",\"content\":\\[' -g '*.jsonl' | head`. Counting rule: matched records.",
"located": {
"claude_code": null,
"csift": "0.7.6",
"source": "src/model/predicates.rs comment; AGENTS.md section 3.2"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "cd ~/.claude/projects && BIG=$(du -sk ./*/ | sort -rn | head -1 | cut -f2-); rg -oNI --no-filename -F '\"type\":\"hook_additional_context\",\"content\":[' \"$BIG\" -g '*.jsonl' | wc -l AND the same with '\"content\":\"' AND csift search '<stamp at=' -c versus csift search '<stamp at=' --additional-context -c AND csift search '<stamp at=' --additional-context --count-by label",
"observed": "In the largest single project dir (2.0 GB, 5,624 jsonl files): 25,666 lines carry `\"type\":\"hook_additional_context\",\"content\":[` (ARRAY) and 0 lines carry `\"type\":\"hook_additional_context\",\"content\":\"` (bare string). Corpus-wide the type has 88,600 attachment records. Gate: a hook-injected phrase returns 28 exchanges on a default scan (all prose mentions) versus 1,394 exchanges with --additional-context, and those hits key 1,612 records under harness.meta.hook.",
"rule": "One count per raw LINE matching the literal key/value/open-bracket triple, scoped to one project dir; the gate test counts exchanges from `-c` with and without the flag on the identical pattern.",
"note": "The array shape is unanimous - 25,666 to 0 in a 2.0 GB scope. The default-off gate is real and measurable: the same pattern goes from 28 to 1,394 exchanges when --additional-context is passed, so a bare-string-only reader would indeed make every hook-injected context invisible."
}
]
},
{
"id": "CLS-020",
"area": "classification",
"behavior": "Claude Code marks a `type:\"attachment\"` record's payload with its own `type` field, and 34 distinct payload types were observed corpus-wide, dominated by `hook_success` (338,770), `hook_additional_context` (88,600), `deferred_tools_delta` (8,167), `skill_listing` (8,060), `task_reminder` (7,308), `total_tokens_reminder` (6,220), `queued_command` (2,356), `edited_text_file` (1,334), `batching_reminder_sent` (948) and `hook_cancelled` (770), down to `plan_mode` (21) and the two rarest, `opened_file_in_ide` and `plan_mode_reentry` (2 each).",
"depends": "csift exposes the payload type as the `--count-by attachment` census key and classifies any non-hook payload `harness.meta.attachment`, with all attachment parsing gated behind `--attachments` / `--additional-context`; without the gate a default scan would parse megabyte attachment lines and blow the performance contract.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "306-309",
"snippet": " pub fn attachment_type(&self) -> Option<String> {\n if !self.is_type(\"attachment\") {\n return None;\n }"
},
{
"path": "src/search/scan.rs",
"lines": "356-357",
"snippet": " static ATTACHMENT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memmem::Finder::new(b\"\\\"attachment\\\"\"));"
}
],
"instrument": "`csift search '' <project-dir> --count-by attachment` (the axis implies the `--attachments` gate), cross-checked with `rg -oNI '\"attachment\":\\{\"type\":\"[a-z_]+\"' -g '*.jsonl' | sort | uniq -c`. Counting rule: one per attachment RECORD keyed by `attachment.type` for csift, one per raw match for rg.",
"located": {
"claude_code": "2.1.258",
"csift": "0.8.1",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' --count-by attachment (whole corpus: 5.1 GB, 7,748 jsonl files, 72 s wall) cross-checked with cd ~/.claude/projects && BIG=$(du -sk ./*/ | sort -rn | head -1 | cut -f2-); rg -oNI --no-filename '\"attachment\":\\{\"type\":\"[a-z_]+\"' \"$BIG\" -g '*.jsonl' | sed 's/.*\"type\":\"//;s/\"//' | sort | uniq -c | sort -rn AND csift search 'batching_reminder_sent' -c versus csift search 'batching_reminder_sent' --attachments --count-by label",
"observed": "34 distinct payload types corpus-wide (the claim's count, unchanged). Fresh counts: hook_success 338,770 - hook_additional_context 88,600 - deferred_tools_delta 8,167 - skill_listing 8,060 - task_reminder 7,308 - total_tokens_reminder 6,220 - queued_command 2,356 - edited_text_file 1,334 - batching_reminder_sent 948 - hook_cancelled 770 - ultrathink_effort 594 - file 577 - compact_file_reference 551 - bash_output_audience_note 422 - date_change 394 - agent_listing_delta 226 - read_truncation_notice 164 - plan_file_reference 151 - invoked_skills 134 - workflow_keyword_request 115 - silent_turn_reminder 95 - hook_non_blocking_error 93 - remote_session_change 55 - command_permissions 48 - hook_blocking_error 30 - plan_mode_exit 23 - diagnostics 21 - plan_mode 21 - task_status 16 - auto_mode 13 - hook_system_message 5 - hook_stopped_continuation 3 - opened_file_in_ide 2 - plan_mode_reentry 2. The rg cross-check in one project dir reproduces the same ordering (hook_success 81,841, hook_additional_context 25,666, deferred_tools_delta 5,749, skill_listing 5,628, task_reminder 2,107, ...). Gate: `batching_reminder_sent` returns 8 exchanges on a default scan and 954 records under harness.meta.attachment with --attachments.",
"rule": "One count per attachment RECORD keyed by attachment.type for csift; one count per raw match of the literal key/type pair for rg. The corpus is live and monotone-growing, so every figure is a snapshot with a timestamp, not a constant.",
"note": "The distinct-key count 34 reproduces exactly and every named type is also a string in the 2.1.258 binary. The per-type counts are all higher than the ledger's by 0.2 to 13 percent except hook_cancelled (770) and plan_mode (21), which are byte-identical - consistent with a few hours of corpus growth on a live machine. The claim's head-of-list also skipped batching_reminder_sent (948), which outranks hook_cancelled; the corrected wording restores it. Any figure in this row needs its snapshot date attached to stay checkable."
}
]
},
{
"id": "CLS-021",
"area": "classification",
"behavior": "Claude Code 2.1.258 writes `type:\"attachment\"` records for 33+ payload kinds whose content is arbitrary nested JSON, up to at least 1699759 bytes (1.7 MB, kind `queued_command`), and they outnumber role-bearing message lines about 2.4:1 (59206 vs 25080 in one project dir). The 337988 hook_success figure is a whole-corpus count and was not re-derived here (scope rule); the rescoped equivalent is 45370 hook_success records in one project dir via `csift search '' . --count-by attachment`.",
"depends": "csift gates the attachment keep behind an `&&`-guarded SIMD needle, so no query that omits `--attachments` / `--additional-context` / `--count-by attachment` ever runs the attachment memmem, and the matched text is the VERBATIM payload JSON - a byte substring of the raw line - so the literal prefilter and the whole-file gate stay sound with no synthesized-marker machinery. The gate is not, however, the only way an attachment line becomes a candidate: under a match-all (no `-t`) scan the D7 `compact_boundary` needle is active, and because that needle is a bare byte substring it also admits any attachment line whose payload prose happens to contain it - measured at 82 of 59206 attachment records (0.14%) in one project dir, surfacing as 64 harness.meta.attachment plus 18 harness.meta.hook.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "322-327",
"snippet": " pub fn attachment_payload_text(&self) -> Option<String> {\n if !self.is_type(\"attachment\") {\n return None;\n }\n self.attachment.as_ref().map(|raw| raw.get().to_string())\n }"
},
{
"path": "src/search/scan.rs",
"lines": "393",
"snippet": " || (needs_attachments && ATTACHMENT_FINDER.find(line).is_some())"
}
],
"instrument": "Functional: `csift search 'hook_success' . -c` (47) vs `... --attachments` (818). Cost: hyperfine --warmup 2 --runs 5 on the same pair; the gated form must be strictly cheaper (measured 253.4 ms / 442 ms user CPU vs 387.5 ms / 872 ms user CPU). Counting rule: wall seconds and user CPU seconds, and matched-exchange count for the functional half.",
"located": {
"claude_code": "2.1.258",
"csift": "0.8.1",
"source": "src/search/scan.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,25}type:\"attachment\".{0,60}' | sort -u || enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); d=~/.claude/projects/$enc; python3 -c 'json-parse every line of $d/*.jsonl; count type==attachment, count records with a dict message{}, max len(json.dumps(attachment))' || csift search '' . --count-by attachment || csift search 'hook_success' . -c vs csift search 'hook_success' . -c --attachments || hyperfine --warmup 2 --runs 5 \"csift search 'hook_success' . -c\" \"csift search 'hook_success' . -c --attachments\"",
"observed": "CC 2.1.258 binary carries three distinct attachment-record writers, e.g. 'function hBn(e){return{type:\"attachment\",attachment:e,uuid:RHo(),timestamp:new Date().toISOString()}}' and '{return{attachment:e,type:\"attachment\",uuid:n.uuid(),timestamp:(e.type===\"queued_command\"||e.type=...'. One project dir under ~/.claude/projects (23 top-level .jsonl, 427 MB): 59206 attachment records vs 25080 lines carrying a role marker -> attachments outnumber message lines 2.36x. `--count-by attachment` over that dir reports 33+ distinct payload kinds, top: hook_success 45370, hook_additional_context 9909, total_tokens_reminder 2549, task_reminder 630, skill_listing 536, deferred_tools_delta 452. Largest attachment payload in that dir: 449848 bytes (kind queued_command); in a second, larger project dir (2.1 GB, 104822 attachment records): 1699759 bytes (kind queued_command) -> megabyte payloads confirmed. Gate is functional: `csift search 'hook_success' . -c` = 47, `... --attachments` = 818. hyperfine: gated 253.4 ms +/- 77.0 (user CPU 442 ms) vs --attachments 387.5 ms +/- 193.0 (user CPU 872 ms) -> the opt-in roughly doubles CPU; the gated form pays none of it. Verbatim-payload check on a synthetic transcript: with --attachments the matched/rendered text is '{\"type\": \"task_reminder\", \"content\": [{\"id\": \"1\", \"subject\": \"alpha reminder\"}]}', a byte substring of the raw line (which was written with whitespace-separated JSON, so the match is serialization-tolerant too); without the flag the same literal returns 0. LEAK MEASURED: a default no-`-t` scan of that project dir still labels 64 records harness.meta.attachment. Cause isolated: under match-all the D7 compact_boundary needle is ON, and it is a bare byte substring that occurs inside attachment payload prose. Exactly 82 attachment records in that dir contain b'compact_boundary'; splitting by payload kind gives 64 'other' (-> harness.meta.attachment, == the census figure) and 18 hook_additional_context (-> harness.meta.hook; 18 + 49 isMeta role records = the census's 67). Proven on a 4-line synthetic transcript: an attachment whose payload prose says 'document the compact_boundary record' IS admitted under no `-t` (1 harness.meta.attachment); siblings saying 'turn_duration' or nothing are NOT; under `-t user` none are.",
"rule": "Attachment / message ratio: count lines in the 23 top-level .jsonl of ONE project dir where json.loads(line)['type']=='attachment', versus lines whose raw bytes match /\"role\"\\s*:\\s*\"(user|assistant)\"/ (csift's own stage-1 needle). Payload size: len(json.dumps(record['attachment'])) per attachment record, take the max. Payload-kind census: one record per attachment line, keyed by attachment.type (csift --count-by attachment). Timing: hyperfine wall-clock mean over 5 runs after 2 warmups, same literal, same scope, flag toggled. Leak: records whose emitted label set contains harness.meta.attachment in a flagless `csift search '' . --count-by label`, one record per line.",
"note": "Behavior and the perf half of the dependency both hold, with numbers corrected: attachment payloads do reach megabyte scale (1.7 MB observed) but not in every project dir (max 0.45 MB in the one first measured), and the attachment/message ratio is 2.4:1, not an order of magnitude. The one substantive correction is that 'a default scan pays zero' is true of the attachment needle only. A flagless scan still parses a small fraction of attachment lines because the always-on-under-match-all compact_boundary needle is a bare value substring; this is a permissive-superset artifact (the extra records classify correctly, nothing is mislabeled), and it is invisible outside a corpus whose prose discusses transcript internals."
}
]
},
{
"id": "CLS-022",
"area": "classification",
"behavior": "The word `attachment` appears verbatim as an attachment record's `type` VALUE and as its payload key, whereas the same word inside message prose is always inside a JSON string where an adjacent quote is escaped to a backslash-quote pair.",
"depends": "csift's attachment needle is the QUOTED form `\"attachment\"`, which is serialization-tolerant and never false-keeps on prose mentioning the word; a bare-word needle would defeat the prefilter on every csift development transcript.",
"code": [
{
"path": "src/search/scan.rs",
"lines": "352-355",
"snippet": " // Quoted needle: `\"attachment\"` appears verbatim as the record's `\"type\"` VALUE (and as\n // its payload KEY); an in-content quote is escaped to `\\\"` in raw bytes, so prose that\n // merely mentions the word never false-keeps. Serialization-tolerant (the quoted value\n // survives a reserialize; R13)."
}
],
"instrument": "On one real transcript, count role/message lines that contain the bare word `attachment` and, of those, how many also contain the unescaped `\"attachment\"`. The second number must be 0 (measured: 587 and 0). Counting rule: one per matching LINE, restricted to records whose message.role is user or assistant.",
"located": {
"claude_code": null,
"csift": "0.8.1",
"source": "src/search/scan.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); f=$(ls -S ~/.claude/projects/$enc/*.jsonl | head -1); rg -cNI '\"attachment\"' $f; rg -cNI 'attachment' $f; then python3 -c 'for each line of $f: r=json.loads(line); isrole = isinstance(r.get(\"message\"),dict) and r[\"message\"][\"role\"] in (user,assistant); tally bare = \"attachment\" in line, quoted = chr(34)+\"attachment\"+chr(34) in line'",
"observed": "One transcript, 46827 lines. rg -cNI '\"attachment\"' = 27161; rg -cNI 'attachment' = 27818; difference 657. Decomposed: 587 role/message lines contain the bare word `attachment` and ZERO of them contain the unescaped quoted form; the remaining 70 are non-role lines that mention it without the quoted form. Line-type census of the same file: attachment 27164, assistant 6851, user 3521, plus metadata types. Escaped form: rg -cNI '\\\\\"attachment\\\\\"' returns no lines, i.e. in-content quotes are present only as the escaped pair and never as the needle bytes.",
"rule": "Per LINE of one transcript. bare = the raw line contains the 10 bytes `attachment`; quoted = the raw line contains the 12 bytes `\"attachment\"`. A false-keep is a line that is bare-true, quoted-true, and whose record is a role-bearing message (message.role in {user, assistant}) - i.e. the quoted needle fired on prose. Count those.",
"note": "The mechanism holds exactly as stated - the word appears unescaped only as a JSON type value or key, and an in-content quote is always the escaped pair - and the false-keep rate on prose is 0 of 587 mentions in a transcript from a corpus that discusses transcript internals constantly. Only the claim's own instrument needed replacing: a bare line-count comparison is dominated by the attachment records themselves and cannot see the property being asserted."
}
]
},
{
"id": "CLS-023",
"area": "classification",
"behavior": "Claude Code's `type:\"system\"` records carry no `message{}` and therefore no role marker, so they are invisible to a role-based byte prefilter, and the five promoted line types are rare relative to message lines.",
"depends": "csift admits each promoted line only when an EXPLICIT `-t` selector reaches its leaf (full path, glob, or the `harness.meta` prefix) or a `show` address is present - stricter than the compaction-boundary keep, which a match-all scan admits - so a bare scan, a bare role selector, a `-T`-only filter and a flagless `--count-by label` never parse them; a zero-match run with no reaching selector emits a stderr note and JSON `gated_leaves_unreached`.",
"code": [
{
"path": "src/search/scan.rs",
"lines": "95-100",
"snippet": " let reach = |c: Class| args.reaches_gated(c) || address.is_some();\n let gates = CandidateGates {\n compact_boundary: needs_compact_boundary,\n hook_context: needs_hook_context,\n attachments: needs_attachments,\n queued: reach(Class::UserQueued),"
},
{
"path": "src/search/scan.rs",
"lines": "395-399",
"snippet": " || (gates.queued && QUEUED_FINDER.find(line).is_some())\n || (gates.turn_duration && TURN_DURATION_FINDER.find(line).is_some())\n || (gates.away_summary && AWAY_SUMMARY_FINDER.find(line).is_some())\n || (gates.stop_hooks && STOP_HOOKS_FINDER.find(line).is_some())\n || (gates.snapshot && SNAPSHOT_FINDER.find(line).is_some())"
}
],
"instrument": "`csift search '' <project-dir> --count-by label` (no `-t`) must show no promoted leaf, while `csift search '' <project-dir> -t harness.meta --count-by label` must show them. Counting rule: one record per line admitted by the gate.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/search/scan.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,45}\"turn_duration\".{0,45}' | sort -u || enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); d=~/.claude/projects/$enc; python3 -c 'json-parse every line of $d/*.jsonl; for type==system tally subtype, whether the record has a message key, whether the raw line contains a \"role\" token' || csift search '' . --count-by label vs csift search '' . -t harness.meta --count-by label vs csift search '' . -t user.queued --count-by label vs csift search '' . -T user --count-by label vs csift search '' . -t harness --count-by label || csift --claude-home $FX search 'nomatchhere' @-fx --format json",
"observed": "CC 2.1.258 constructors, all message-less: 'B_t(e,n,r,o,d){return{type:\"system\",subtype:\"turn_duration\",durationMs:e,budgetTokens:n?.tokens,budgetLi...'; '{return{type:\"system\",subtype:\"stop_hook_summary\",hookCount:e,hookInfos:n,hookErrors:r,ho...'; 'function DXn(e){return{type:\"system\",subtype:\"away_summary\",content:e,timestamp:new Date().toISOString(),uuid:...}'; 'o(Br,br,Ts){let qi={type:\"queue-operation\",operation:Br,timestamp:new Date().toISOString(),sessionId:Q(),...'. Corpus, 23 top-level .jsonl of one project dir: 863 system records (stop_hook_summary 410, turn_duration 284, away_summary 147, compact_boundary 17, agents_killed 3, model_refusal_fallback 1, scheduled_task_fire 1); 0 carry a `message` key; 0 contain a `\"role\"` token, so the role prefilter cannot see any of them. Rarity: the five promoted types total 2496 lines (turn_duration 284 + away_summary 147 + stop_hook_summary 410 + queue-operation 1083 + file-history-* 572) against 25080 role-bearing lines = 9.95%. Gate: flagless `--count-by label` emits 23 leaf keys and NONE of the five promoted ones. `-t harness.meta` emits harness.meta.snapshot 572, harness.meta.stop-hooks 410, harness.meta.turn-duration 284, harness.meta.away-summary 147 - each EXACTLY equal to the independent rg line count for the corresponding raw type/subtype. `-t user.queued` = 60 (of 1083 queue-operation lines; the rest are content-less or harness riders). `-T user` (exclude-only) emits none of the five. `-t harness` (bare role) also emits none of the five, consistent with Class::llm_visible gating role expansion. Zero-match diagnosis on a synthetic transcript: stderr prints 'csift: note: the gated leaves (user.queued, harness.meta.turn-duration, harness.meta.away-summary, harness.meta.stop-hooks, harness.meta.snapshot) are scanned only under an explicit -t that reaches them - this absence does not cover those lines.' and the JSON summary carries \"gated_leaves_unreached\": true; the same query with `-t harness.meta` carries \"gated_leaves_unreached\": false.",
"rule": "message-less claim: of all lines in one project dir's top-level .jsonl whose parsed type is `system`, count those with a `message` key (expect 0) and those whose raw bytes match /\"role\"\\s*:\\s*\"(user|assistant)\"/ (expect 0). Rarity: sum the five promoted raw line counts via rg on `\"type\":\\s*\"...\"` / `\"subtype\":\\s*\"...\"`, divide by the role-marker line count. Gate: one record per line admitted; compare the leaf keys emitted by `--count-by label` with and without each selector, and cross-check each promoted leaf's count against the independent rg count of its raw line type.",
"note": "Both halves confirmed by independent instruments. The message-less property is visible in CC 2.1.258's own record constructors (no `message` field in any of the four) and in the corpus (0 of 863 system records carry one, 0 carry a role token). The admission rule is exactly as claimed and stricter than the boundary keep: flagless, bare-role and exclude-only selectors all fail to reach the five leaves, while `harness.meta` and full leaf paths reach them and reproduce the raw line counts to the record. The unreached-leaf disclosure fires as described in both text and JSON."
}
]
},
{
"id": "CLS-024",
"area": "classification",
"behavior": "Claude Code's promoted non-record lines carry no text spelling out their rendered facts: a `turn_duration` record is {durationMs, messageCount} integers plus ids, `stop_hook_summary` is {hookCount, hookInfos, hookErrors, preventedContinuation} and `file-history-*` are paths and version numbers - 0 of 284 turn_duration, 0 of 410 stop_hook_summary and 0 of 139 file-history-delta records contain the bracketed strings csift renders. csift fabricates FOUR such forms, not three: `[turn duration: ...]`, `[stop hooks: ...]`, `[file-history delta at ...]` and `[file-history snapshot at ...]`, the last two both under the single `harness.meta.snapshot` marker `file-history-`.",
"depends": "csift registers each line's own type/subtype bytes (`turn_duration`, `stop_hook_summary`, `file-history-`) as VERIFIABLE synthesized-text markers, active only under the explicit selector that admits the line; without a marker the whole-file prefilter gate would silently drop matches against those fabricated excerpts. The queued and away-summary leaves render VERBATIM content and need no marker.",
"code": [
{
"path": "src/search/matcher.rs",
"lines": "502-505",
"snippet": " // v0.10.0 promoted lines whose render FABRICATES text (key=value excerpts): the\n // marker is the line's own type/subtype value, active only when the explicit\n // selector admits the line (otherwise it is not a candidate at all). The queued\n // and away-summary leaves render VERBATIM content and need no marker."
}
],
"instrument": "`csift search 'turn duration' . -t harness.meta.turn-duration -c` must return hits (measured 270 exchanges over 284 records) while a per-RECORD probe shows 0 of 284 turn_duration records contain the phrase in their raw bytes. Counting rule: exchanges for csift, records for the probe. Do NOT use a whole-file `rg -c 'turn duration'` as the negative side - it returns 56 on a transcript whose prose discusses the feature.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/search/matcher.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); d=~/.claude/projects/$enc; rg -NI --no-filename -c 'turn duration' $d/*.jsonl | paste -sd+ - | bc || python3 -c 'json-parse every line of $d/*.jsonl; bucket by (type, subtype); per bucket count records whose RAW line contains the rendered phrase' || csift search 'turn duration' . -t harness.meta.turn-duration -c || csift search '' . -t harness.meta.turn-duration --max-count 2 || csift search 'file-history delta' . -t harness.meta.snapshot --max-count 2 || grep -rn 'file-history snapshot at\\|file-history delta\\|turn duration:\\|stop hooks:' src/",
"observed": "Per-record probe over one project dir's top-level .jsonl: turn_duration 284 records, 0 whose raw line contains 'turn duration' (with the space); stop_hook_summary 410 records, 0 containing 'stop hooks'; file-history-delta 139 records, 0 containing 'file-history delta at'. A raw turn_duration record is fields only: {type, subtype, durationMs: 446815, messageCount: 105, timestamp, uuid, isMeta, userType, entrypoint, cwd, sessionId, version, gitBranch}. csift renders the fabricated excerpts: '[turn duration: 10m 29s . durationMs=629054 messageCount=266]', '[stop hooks: count=6 errors=0 prevented=false]', '[file-history delta at 2026-07-14T03:00:27.193Z: <path>@v1]', '[file-history snapshot at <ts>: <path>@v2, ...]'. `csift search 'turn duration' . -t harness.meta.turn-duration -c` = 270 (exchanges; several turn_duration records share a turn, so 270 exchanges cover 284 records). The by-file rg, however, returns 56 - the phrase occurs as ordinary prose in a corpus that documents the feature. away_summary renders VERBATIM record content (observed: the summary sentence itself, no bracket), consistent with it needing no marker.",
"rule": "Restrict to the promoted records themselves, not the file: of the N records whose parsed (type, subtype) is the promoted kind, count those whose RAW line contains the rendered phrase - expect 0/N (measured 0/284, 0/410, 0/139). Then count csift hits for that phrase under the leaf's explicit selector - expect > 0 (measured 270 exchanges). A whole-file rg is not a valid denominator: prose about the feature also matches.",
"note": "The behavior holds at record granularity, which is the granularity the synthesized-marker machinery actually needs: none of the three fabricated-text kinds can be found by matching the raw line, so without a registered marker the whole-file prefilter gate would drop those matches. Two corrections: the claim's negative instrument is stated at file granularity and fails there (56 prose lines match), and the snapshot render `[file-history snapshot at ...]` is a fourth fabricated form the claim omits, covered by the same `file-history-` marker."
}
]
},
{
"id": "CLS-025",
"area": "classification",
"behavior": "`file-history-snapshot` and `file-history-delta` lines carry no `message` object - the boundary instrument for LLM visibility - so they are machinery records rather than conversation records.",
"depends": "csift maps BOTH line types to the single gated leaf `harness.meta.snapshot` with `Class::llm_visible` false, and its byte prefilter admits them only under an explicit `-t` selector reaching the leaf or a `show` address, so a bare scan or a `-T`-only filter never parses them and the performance contract holds.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "27",
"snippet": "\"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),"
},
{
"path": "src/search/scan.rs",
"lines": "327",
"snippet": "/// v0.10.0: `file-history-snapshot` + `file-history-delta` lines."
}
],
"instrument": "`csift search '' <project-token> -t harness.meta.snapshot --count-by label` versus the same query without `-t`: the leaf must appear only under the explicit selector. Counting rule: one record per line admitted by the gate.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "AGENTS.md section 3.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,30}\"file-history-snapshot\".{0,50}' | sort -u; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'file-history-(snapshot|delta)[^ ]{0,40}' | sort -u || enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); d=~/.claude/projects/$enc; python3 -c 'json-parse every line of $d/*.jsonl; for type in file-history-snapshot, file-history-delta count records and how many carry a message key' || csift search '' . -t harness.meta.snapshot --count-by label vs csift search '' . --count-by label | rg -c 'harness.meta.snapshot' || csift --claude-home $FX search '' @-fx -t harness.meta.snapshot --count-by label vs csift --claude-home $FX search '' @-fx --count-by label",
"observed": "CC 2.1.258 writers, both message-less: 'ckWrite(async()=>{let d={type:\"file-history-snapshot\",messageId:e,snapshot:n,isSnapshotUpdate:r};await ' and 'file-history-delta\",messageId:e,snapshotMessageId:n,tracki...'. Corpus, 23 top-level .jsonl of one project dir: 433 file-history-snapshot + 139 file-history-delta = 572 records, 0 carrying a `message` key. Observed raw shapes: a snapshot line is {type, messageId, snapshot, isSnapshotUpdate} - it has no top-level `timestamp` and no `uuid` either; a delta line is {type, messageId, snapshotMessageId, trackingPath, backup, timestamp}. Single-leaf mapping and gate, live: `-t harness.meta.snapshot --count-by label` = 572 (== 433 + 139, the two types folded into one key), while a flagless `--count-by label` emits no harness.meta.snapshot key at all (grep count 0). Re-run minutes later on the still-growing corpus returned 575, same rule. Controlled fixture with exactly one snapshot line and one delta line: `-t harness.meta.snapshot --count-by label` = 2 under the one key; flagless `--count-by label` emits only agent.message 1 and user.message 1.",
"rule": "message-less: of the lines in one project dir's top-level .jsonl whose parsed type is file-history-snapshot or file-history-delta, count those with a `message` key (expect 0 of 572). Single leaf: the count under `-t harness.meta.snapshot` must equal the sum of the two independent rg line counts. Gate: one record per line admitted; the leaf key must be absent from a flagless `--count-by label` and present under the explicit selector. Corpus counts drift upward on a live corpus - the rule, not the number, is the invariant.",
"note": "Confirmed from both ends. CC 2.1.258's own writers for the two line types construct objects with no `message` field, and no such record in the sampled project dir carries one (0 of 572) - so the LLM-visibility boundary instrument the claim names is genuinely absent, not merely unobserved. The single-leaf mapping is exact: 433 snapshots plus 139 deltas fold to one key of 572, reproduced on a two-line synthetic fixture as a key of 2. The gate behaves as claimed - the leaf is unreachable without an explicit selector that names it."
}
]
},
{
"id": "CMP-001",
"area": "compaction",
"behavior": "A compaction SUMMARY is a `type:\"user\"` record carrying `isCompactSummary:true` co-set with `isVisibleInTranscriptOnly:true`, whose `message.content` is a bare STRING that commonly opens `This session is being continued from a previous conversation...`; Claude Code writes no `type:\"summary\"` record at all.",
"depends": "csift keys compaction detection on the `isCompactSummary` FLAG (never on text), excludes the summary from `is_genuine_user` so it never opens a turn, labels it `harness.compaction.summary`, and counts it as the `compactions` figure in `stats`; `verbatim` uses it as the anchor for the turns it clipped. Keying on a `summary` record type finds nothing in current data, and treating the summary as a human turn injects a machine recap into the operator census.",
"code": [
{
"path": "src/model/record.rs",
"lines": "66-68",
"snippet": " /// Compaction summary marker - when true, this user record is NOT a human turn.\n #[serde(default, rename = \"isCompactSummary\")]\n pub is_compact_summary: Option<bool>,"
},
{
"path": "src/model/record.rs",
"lines": "77-79",
"snippet": " /// Co-set with `isCompactSummary` on compaction-summary records (§4.7).\n #[serde(default, rename = \"isVisibleInTranscriptOnly\")]\n pub is_visible_in_transcript_only: Option<bool>,"
},
{
"path": "src/model/predicates.rs",
"lines": "41-43",
"snippet": " if self.is_compact_summary.unwrap_or(false) {\n return false;\n }"
},
{
"path": "src/model/classify.rs",
"lines": "238-241",
"snippet": " if self.is_compact_summary.unwrap_or(false) {\n push_unique(out, Class::CompactionSummary);\n return;\n }"
}
],
"instrument": "`jq -r 'select(.isCompactSummary==true) | [.type, .isVisibleInTranscriptOnly] | @tsv' <transcript>` must print `user true` on every hit, and `rg -c '\"type\":\"summary\"' ~/.claude/projects -g '*.jsonl'` must return nothing; cross-check `csift search '' @<session> -t harness.compaction.summary -c` against `csift stats @<session> --format json | jq .compactions`. Counting rule: one summary record per compaction event, counted per raw line.",
"located": {
"claude_code": "2.1.231",
"csift": "0.1.0",
"source": "SPEC.md sections 3.1 and 4.7; SPEC.md section 5; AGENTS.md section 3.5; src/model.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"isCompactSummary\":true' -g '*.jsonl' . | jq -r 'select(.isCompactSummary==true)|[.type,(has(\"isVisibleInTranscriptOnly\")|tostring),(.isVisibleInTranscriptOnly|tostring),(.message.content|type)]|@tsv' | sort | uniq -c ; rg -l '\"type\":\"summary\"' -g '*.jsonl' . ; rg -NI --no-filename '\"isCompactSummary\":true' -g '*.jsonl' . | jq -r 'select(.isCompactSummary==true)|.message.content[0:70]' | sort | uniq -c ; csift search '' @<session> -t harness.compaction.summary -c --no-subagents ; csift stats @<session> --no-subagents --format json | jq -c '{turns,compactions}' ; csift search '' @<session> -t harness.compaction.summary --format json --no-subagents --max-count 1 | jq -c 'select(.kind==\"exchange\")|{turn_index,labels:[.hits[].labels[]],line:[.hits[].line]}'",
"observed": "232 of 232 parsed summary records: type=user, isVisibleInTranscriptOnly=true, message.content type=string. 232/232 open with the same 70 characters, 'This session is being continued from a previous conversation that ran'. rg -l '\"type\":\"summary\"' over the whole corpus exits 1 with no file listed. On one session csift reports 5 harness.compaction.summary records and stats reports compactions=5, turns=53; the summary at L4807 carries the single label harness.compaction.summary and sits INSIDE turn 15 (a member, not an opener).",
"rule": "One row per raw jsonl line whose parsed isCompactSummary is boolean true, corpus-wide over ~/.claude/projects (5.1 GB, 7,753 jsonl files); csift counts one record per matched line under --no-subagents.",
"note": "Verified verbatim in the current tree at the claimed line numbers: src/model/record.rs:65-67 and :76-78, src/model/predicates.rs:41-43, src/model/classify.rs:238-241 (inside classify_user). Nothing moved."
}
]
},
{
"id": "CMP-002",
"area": "compaction",
"behavior": "Beside the summary Claude Code writes a separate `type:\"system\"` record with `subtype:\"compact_boundary\"`, one per compaction event, carrying a top-level `content:\"Conversation compacted...\"` string plus `compactMetadata:{trigger:\"auto\"|\"manual\", preTokens, postTokens, durationMs, cumulativeDroppedTokens, preCompactDiscoveredTools[], preservedSegment:{headUuid,anchorUuid,tailUuid}, preservedMessages:{anchorUuid,uuids[],allUuids[]}}`. `cumulativeDroppedTokens` is the newest scalar (absent up to CC 2.1.191, present on every boundary from 2.1.199 through 2.1.258); `preCompactDiscoveredTools` and the `preservedMessages.allUuids` array are each absent on a handful of older boundaries; a short-lived `precomputed` key appeared on 6 boundaries around CC 2.1.191-2.1.202 and on none since.",
"depends": "csift identifies the boundary purely by `type` + `subtype`, keeps `compactMetadata` UNPARSED, and renders the four scalar keys as `[compaction boundary: trigger=... preTokens=... postTokens=... durationMs=...]`, which is what makes `-t harness.compaction.boundary` both matchable and inspectable; the extra prefilter keep for this role-marker-less line is `-t`-gated behind `needs_compact_boundary`, so every other query pays zero.",
"code": [
{
"path": "src/model/record.rs",
"lines": "97-102",
"snippet": " /// `compact_boundary` metrics (§3.5 / D7): `{trigger, preTokens, postTokens, durationMs}` on a\n /// `type:\"system\"`/`subtype:\"compact_boundary\"` record. Kept RAW; `search` renders it as a\n /// readable excerpt (`record_raw_text`) so `-t harness.compaction.boundary` can enumerate\n /// compaction points and inspect what each clipped. Absent on every other record. Tolerant.\n #[serde(default, rename = \"compactMetadata\")]\n pub compact_metadata: Option<serde_json::Value>,\n"
},
{
"path": "src/model/classify.rs",
"lines": "7-12",
"snippet": " /// True when this is the `system`/`compact_boundary` metrics record (GOLD §5) -\n /// `harness.compaction.boundary`.\n #[must_use]\n pub fn is_compact_boundary(&self) -> bool {\n self.is_type(\"system\") && self.subtype.as_deref() == Some(\"compact_boundary\")\n }"
},
{
"path": "src/search/record_text.rs",
"lines": "311-319",
"snippet": "/// Render a `compact_boundary` record's `compactMetadata` object as a one-line readable excerpt -\n/// `[compaction boundary: trigger=auto preTokens=1000 postTokens=200 durationMs=50]` (only the\n/// present fields, stable order, scalars unquoted). `None` when it is not an object or carries none\n/// of the known fields.\npub(crate) fn compact_metadata_excerpt(meta: &serde_json::Value) -> Option<String> {\n let obj = meta.as_object()?;\n let mut fields: Vec<String> = Vec::new();\n for key in [\"trigger\", \"preTokens\", \"postTokens\", \"durationMs\"] {\n if let Some(v) = obj.get(key) {"
},
{
"path": "src/search/scan.rs",
"lines": "386",
"snippet": " || (needs_compact_boundary && COMPACT_BOUNDARY_FINDER.find(line).is_some())"
}
],
"instrument": "`jq -r 'select(.subtype==\"compact_boundary\") | (.compactMetadata|keys|join(\",\"))' <transcript> | sort | uniq -c` enumerates the metadata keys actually present; `csift search '' @<session> -t harness.compaction.boundary` must print a `[compaction boundary: trigger=... preTokens=...]` excerpt per hit. Counting rule: one boundary record per compaction event; the boundary count must track the summary count on the same file.",
"located": {
"claude_code": "2.1.231",
"csift": "0.2.0",
"source": "SPEC.md section 4.7; AGENTS.md section 3.5; CHANGELOG 0.8.1; src/model/record.rs compactMetadata comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|(.compactMetadata|keys|join(\",\"))' | sort | uniq -c ; same stream piped through jq for .compactMetadata.trigger, for the preservedSegment/preservedMessages key sets, for .content[0:40], and for [(.version),(.compactMetadata|has(\"cumulativeDroppedTokens\")),(.compactMetadata|has(\"precomputed\"))] ; per-file pairing script counting boundaries vs summaries ; csift search '' @<session> -t harness.compaction.boundary --no-subagents --max-count 2",
"observed": "232 boundary records corpus-wide. Key sets: 130 carry cumulativeDroppedTokens,durationMs,postTokens,preCompactDiscoveredTools,preTokens,preservedMessages,preservedSegment,trigger; 91 the same minus cumulativeDroppedTokens; 4 minus preCompactDiscoveredTools; 6 also carry precomputed. trigger: auto 217 / manual 15. preservedSegment keys anchorUuid,headUuid,tailUuid on 232/232. preservedMessages keys allUuids,anchorUuid,uuids on 231/232 (one legacy record has only anchorUuid,uuids). content begins 'Conversation compacted' on 232/232. cumulativeDroppedTokens is absent on every boundary written by CC <= 2.1.191 and present on every boundary from 2.1.199 through 2.1.258; precomputed appears only on 6 boundaries at 2.1.191/2.1.199/2.1.202 and on none since. Boundary count equals summary count on 23 of 23 files that contain either (232 = 232). csift renders each hit as: 'Conversation compacted [compaction boundary: trigger=manual preTokens=787488 postTokens=22073 durationMs=168904] [logicalParent=<uuid>]'.",
"rule": "One row per raw jsonl line whose parsed subtype is compact_boundary, corpus-wide; key sets compared as the sorted comma-joined key list of the compactMetadata object; the pairing rule counts, per file, lines whose parsed subtype is compact_boundary against lines whose parsed isCompactSummary is true.",
"note": "The claim's key list is a subset of what current CC writes: it omits cumulativeDroppedTokens, which is on 100% of boundaries at 2.1.199 and later including the one written by 2.1.258. csift keeps compactMetadata unparsed and renders only the four scalars trigger/preTokens/postTokens/durationMs, so the extra keys change nothing in the render - but a reader treating the claim's list as exhaustive would be wrong. Code verified verbatim at the claimed lines: src/model/record.rs:90-95, src/model/classify.rs:7-12, src/search/record_text.rs:288-296, src/search/scan.rs:370."
}
]
},
{
"id": "CMP-003",
"area": "compaction",
"behavior": "A compaction boundary's own `parentUuid` is null; the true predecessor record the compaction re-links to is named by a TOP-LEVEL `logicalParentUuid` on the boundary - the harness's own ground truth for rejoining the conversation chain across a compaction.",
"depends": "csift models `logical_parent_uuid` tolerantly and appends `[logicalParent=<uuid>]` to the boundary's rendered line, so a reader can rejoin the graph across a boundary; a walker that trusted `parentUuid` alone would see the post-compaction chain start from nothing.",
"code": [
{
"path": "src/model/record.rs",
"lines": "104-109",
"snippet": " /// `logicalParentUuid` (top-level on `compact_boundary` system records): the TRUE\n /// predecessor record the compaction re-links to (`parentUuid` is null on a\n /// boundary) - harness ground truth for the post-compaction chain. Additive +\n /// tolerant.\n #[serde(default, rename = \"logicalParentUuid\")]\n pub logical_parent_uuid: Option<String>,"
},
{
"path": "src/search/record_text.rs",
"lines": "164-170",
"snippet": " // A boundary's `parentUuid` is null; `logicalParentUuid` names the record the\n // compaction re-links to (harness ground truth) - surface it on the same line.\n if rec.is_compact_boundary() {\n if let Some(lp) = rec.logical_parent_uuid.as_deref() {\n parts.push(format!(\"[logicalParent={lp}]\"));\n }\n }"
}
],
"instrument": "`csift search '' @<session> -t harness.compaction.boundary --max-count 1` then `csift show @<session> --line <n> --raw`: the raw line must carry a top-level `logicalParentUuid` and a null `parentUuid`. Bulk check: `rg -NI '\"subtype\":\"compact_boundary\"' <transcript> | rg -c '\"parentUuid\":null'` must equal the boundary count. Counting rule: one boundary record per compaction event.",
"located": {
"claude_code": "2.1.231",
"csift": "0.8.1",
"source": "SPEC.md section 4.7; AGENTS.md section 3.5; src/model/record.rs logicalParentUuid comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|[(if has(\"parentUuid\") then (.parentUuid|if .==null then \"parentUuid:null\" else \"parentUuid:set\" end) else \"parentUuid:absent\" end),(if has(\"logicalParentUuid\") then (.logicalParentUuid|if .==null then \"lpu:null\" else \"lpu:set\" end) else \"lpu:absent\" end)]|@tsv' | sort | uniq -c ; csift search '' @<session> -t harness.compaction.boundary --no-subagents --max-count 2",
"observed": "232 of 232 boundaries: parentUuid present and null, logicalParentUuid present and non-null. The split is clean across every CC version in the corpus, 2.1.150 through 2.1.258 (the single 2.1.258 boundary included). csift's rendered boundary line ends with '[logicalParent=<uuid>]' as designed.",
"rule": "One row per raw jsonl line whose parsed subtype is compact_boundary, corpus-wide; parentUuid classified by key presence and null-ness, never by byte match.",
"note": "Verified verbatim at the claimed lines: src/model/record.rs:97-102 and src/search/record_text.rs:163-169. The claim's own bulk check ('rg -c \"parentUuid\":null' on boundary lines) works but is a byte test; the parsed form above is the stricter counting rule and gives the same 232."
}
]
},
{
"id": "CMP-004",
"area": "compaction",
"behavior": "The compaction boundary is a record with NO `message{}` field at all - pure compaction metrics - so its only readable text is the top-level `content` string plus the `compactMetadata` object.",
"depends": "csift's record text falls back to that message-less path so `-t harness.compaction.boundary` is search-reachable at all, and the absent `message{}` is the ONE LLM-visibility instrument (`Class::llm_visible` is false for the boundary, so a bare role selector skips it while an explicit path reaches it); because the excerpt is fabricated, `compact_boundary` is registered as a `-t`-gated VERIFIABLE synth marker so the whole-file gate can still prove a non-match.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "165-170",
"snippet": " /// drew real replies before the retraction.)\n /// - `harness.compaction.boundary`: a system record with NO message field at\n /// all - pure compaction metrics. (The compaction SUMMARY is visible: the\n /// DAG threads through it; `isVisibleInTranscriptOnly` is a display flag on\n /// summaries, not a delivery flag, and `isMeta` is an authorship flag -\n /// neither is a visibility instrument.)"
},
{
"path": "src/search/record_text.rs",
"lines": "146-149",
"snippet": "/// Combines the top-level `content` string (`\"Conversation compacted …\"`) with a readable\n/// `compactMetadata` excerpt so `-t harness.compaction.boundary` can both MATCH the boundary and SEE\n/// what each compaction clipped. `None` when neither is present (no fabricated text).\npub(crate) fn system_record_text(rec: &Record) -> Option<String> {"
},
{
"path": "src/search/matcher.rs",
"lines": "496-501",
"snippet": " if args\n .label_filter()\n .selected(Class::CompactionBoundary.path())\n {\n verifiable.push(b\"compact_boundary\");\n }"
}
],
"instrument": "`rg -NI '\"subtype\":\"compact_boundary\"' <transcript> | jq 'has(\"message\")'` must be false on every line, while the same test on any `user`/`assistant` record is true; `csift search 'trigger' @<session> -t harness.compaction.boundary` must still return hits. Counting rule: one boundary record per compaction event.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.1",
"source": "SPEC.md section 6 v0.9.4 ledger item 7; AGENTS.md section 3.5; src/model/taxonomy.rs llm_visible comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|(has(\"message\")|tostring)' | sort | uniq -c ; same stream | jq -r 'select(.subtype==\"compact_boundary\" and .version==\"2.1.258\")|keys|join(\",\")' ; csift search 'trigger' @<session> -t harness.compaction.boundary -c --no-subagents ; sed -n '175,186p' src/model/taxonomy.rs",
"observed": "has(\"message\") is false on 232 of 232 boundaries. The 2.1.258 boundary's complete top-level key list is compactMetadata,content,cwd,entrypoint,gitBranch,isSidechain,level,logicalParentUuid,parentUuid,sessionId,slug,subtype,timestamp,type,userType,uuid,version - no message. Searching the literal 'trigger' under -t harness.compaction.boundary still returns all 5 boundaries of the test session, so the fabricated compactMetadata excerpt is matchable through the synth-marker path. Class::llm_visible returns false for Class::CompactionBoundary and true for Class::CompactionSummary (the invisible set is UserUnsent, UserQueued, CompactionBoundary, MetaTurnDuration, MetaAwaySummary, MetaStopHooks, MetaSnapshot).",
"rule": "One row per raw jsonl line whose parsed subtype is compact_boundary, corpus-wide; message-presence tested as a parsed top-level key, not a byte match.",
"note": "Verified verbatim at the claimed lines: src/model/taxonomy.rs:157-162, src/search/record_text.rs:145-148, src/search/matcher.rs:493-498. The 'trigger' search is the load-bearing check: it proves the -t-gated verifiable synth marker still lets the whole-file gate admit a boundary-only match."
}
]
},
{
"id": "CMP-005",
"area": "compaction",
"behavior": "Optional fields ride the boundary record: 43% of measured boundaries (101/232) carry an `isMeta` key whose value is always `false` - never `true` - and that key is a legacy/lane artifact (98 main-thread boundaries from CC <= 2.1.191 plus 3 subagent boundaries at 2.1.219; CC 2.1.199 onward, 2.1.258 included, writes no `isMeta` on a main-thread boundary at all); about 1% (3/232) carry an `agentId`, and those are exactly the `isSidechain:true` subagent boundaries (first seen CC 2.1.219); a boundary written by a background job carries `sessionKind` (1/232, value `bg`, CC 2.1.252).",
"depends": "csift classifies the boundary on `type` + `subtype` alone, so an `isMeta`, `agentId` or `sessionKind` rider never reroutes it out of `harness.compaction.boundary`. Note the rationale the claim gave is wrong in one detail: because the rider's value is always `false`, a classifier gating on `isMeta==true` (as the genuine-user path does) would drop 0 boundaries, not 43% - the real hazard is a classifier gating on key PRESENCE.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "176-181",
"snippet": " match self.r#type.as_deref() {\n Some(\"system\") => {\n if self.is_compact_boundary() {\n push_unique(&mut out, Class::CompactionBoundary);\n }\n }"
}
],
"instrument": "`rg -NI '\"subtype\":\"compact_boundary\"' ~/.claude/projects -g '*.jsonl' | jq -r '[(.isMeta//false),(.agentId!=null),(.sessionKind//\"-\")] | @tsv' | sort | uniq -c`. Counting rule: one row per boundary record; denominator = all boundary records in scope.",
"located": {
"claude_code": "2.1.219",
"csift": "0.6.1",
"source": "AGENTS.md section 3.5; csift dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|[(has(\"isMeta\")|tostring),(.isMeta|tostring)]|@tsv' | sort | uniq -c ; same stream | jq -r '...|[(.version),(has(\"isMeta\")),(.isMeta)]|@tsv' | sort | uniq -c ; same stream | jq -r '...|[(.isSidechain),(has(\"isMeta\")),(has(\"agentId\"))]|@tsv' | sort | uniq -c ; same stream | jq -r 'select(has(\"sessionKind\"))|[.sessionKind,.version]|@tsv' | sort | uniq -c",
"observed": "Of 232 boundaries: 101 (43.5%) carry an isMeta key and 131 do not - but the VALUE is false on 101 of 101 and true on 0 of 232. The 101 split by lane and version: 98 main-thread boundaries all written by CC 2.1.191 or earlier, plus 3 isSidechain:true subagent boundaries written by 2.1.219. Every main-thread boundary from 2.1.199 onward, including the 2.1.258 one, carries no isMeta key at all. agentId is present on 3 of 232 (1.3%), and those 3 are exactly the isSidechain:true boundaries at 2.1.219. sessionKind is present on 1 of 232, value 'bg', written by 2.1.252.",
"rule": "One row per raw jsonl line whose parsed subtype is compact_boundary; denominator 232 = all boundary records in the corpus. isMeta counted twice: by key presence and by boolean value.",
"note": "The 43% number reproduces exactly under a key-presence counting rule, so the claim is not refuted, but two things needed correcting: isMeta is never true on a boundary, and current CC no longer writes the key on main-thread boundaries. Code verified verbatim at src/model/classify.rs:176-181."
}
]
},
{
"id": "CMP-006",
"area": "compaction",
"behavior": "`compactMetadata.preservedMessages.uuids` is a TAIL WINDOW of what a compaction kept, not a visibility filter: measured over 173 boundaries, render-internal system subtypes appear in it at rates equal to conversation records (`stop_hook_summary` 46/3,848 = 1.20%, `turn_duration` 38/3,205 = 1.19%, `away_summary` 9/1,142 = 0.79%, versus `user` 279/39,583 = 0.70% and `assistant` 594/79,382 = 0.75%); later user records even name a `turn_duration` uuid as their `parentUuid`, so DAG threading is no instrument either.",
"depends": "csift refuses `preservedMessages` membership and DAG threading as LLM-visibility instruments and uses `message{}` presence instead; keying `llm_visible` off `preservedMessages` would mark render-internal telemetry as delivered conversation.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "172-178",
"snippet": " /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n /// over every non-record line type in the corpus; every user/assistant record\n /// does), and Claude Code's\n /// own source labels them REPL-render internals. DAG threading is NOT the\n /// instrument here - later user records name a `turn_duration` uuid as parentUuid\n /// (chain continuity), and `preservedMessages` lists these uuids at the same rate"
}
],
"instrument": "For each `compact_boundary`, join `compactMetadata.preservedMessages.uuids` against the uuids of the lines preceding that boundary in the same file, bucketed by `type`/`subtype`. Counting rule: per line, per boundary window; denominator = uuid-bearing lines in that boundary's own pre-window. Expect rate parity across types.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger item 7; csift dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Python join over the 23 corpus files that contain a compact_boundary (jq per file emitting uuid/type/subtype/preservedMessages.uuids per line): for each boundary, bucket the uuid-bearing lines of its window by type/subtype and count how many of those uuids appear in that boundary's preservedMessages.uuids. Window = the lines since the previous boundary in the same file. Separately, a second Python pass building a parentUuid->child type histogram over the same files.",
"observed": "232 boundaries over 23 files. Membership rate, kept/window: system/turn_duration 55/4,190 = 1.31%; system/stop_hook_summary 63/5,072 = 1.24%; system/away_summary 14/1,434 = 0.98%; assistant 1,007/122,723 = 0.82%; user 466/61,693 = 0.76%; attachment 1,361/396,386 = 0.34%. Render-internal system subtypes are preserved at a rate equal to or slightly above conversation records - the ordering the claim reports, reproduced with the same counting rule on a grown corpus (the claim's 1.20 / 1.19 / 0.79 / 0.70 / 0.75 percentages land within 0.2 points). Narrowing the window to the span the preserved set actually covers makes the parity starker: away_summary 14/16 = 87.5%, stop_hook_summary 64/80 = 80.0%, turn_duration 56/73 = 76.7%, assistant 1,012/1,403 = 72.1%, user 467/849 = 55.0%. The preserved set is also not a contiguous run: on 191 of 232 boundaries the preserved uuids have gaps (median 6 preserved uuids spread over a span of 11 lines, reaching back a median of 20 lines from the boundary). DAG threading: system/turn_duration is named as parentUuid by a later user record 3,088 times, by a system/away_summary record 1,357 times and by an assistant record 5 times; system/away_summary is named by a later user record 1,591 times.",
"rule": "Numerator/denominator per line, per boundary window (windows overlap across boundaries in one file by design); denominator = uuid-bearing lines in that boundary's own window, bucketed by type or type/subtype. Parent histogram: one row per line carrying a parentUuid that resolves to a uuid in the same file.",
"note": "Both halves of the claim reproduce: rate parity across types, and later user records naming a turn_duration uuid as parentUuid. Code verified verbatim at src/model/taxonomy.rs:164-170."
}
]
},
{
"id": "CMP-007",
"area": "compaction",
"behavior": "`compactMetadata.preservedMessages` EXCLUDES every superseded-draft uuid: 0 of 772 measured draft uuids appear in any preservedMessages array.",
"depends": "That exclusion is the instrument behind csift labelling `user.unsent` LLM-invisible; read together with the tail-window rate parity it means absence from preservedMessages carries information while presence does not.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "160-164",
"snippet": " /// - `user.unsent`: a superseded draft is NOT in the surviving conversation -\n /// Claude Code's own `compactMetadata.preservedMessages` accounting excludes\n /// every draft uuid (0 of 772 measured), and the conversation DAG threads\n /// through the resend sibling, never the draft. (Wording law: \"not in the\n /// surviving conversation\", never \"the model never saw it\" - a few drafts"
}
],
"instrument": "Collect draft uuids with `csift search '' @<session> -t user.unsent --format json | jq -r .uuid`, then `rg -o 'preservedMessages\":\\[[^]]*' <transcript>` and test membership. Counting rule: draft uuids found in any preservedMessages array over all draft uuids measured.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger item 7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "For the 6 project directories that contain a compact_boundary: csift search '' @<project dir> -t user.unsent --format json --max-count 0 | jq -r '.hits[]?.uuid' | sort -u (815 uuids). Then cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|((.compactMetadata.preservedMessages.uuids//[]) + (.compactMetadata.preservedMessages.allUuids//[]))[]' | sort -u (3,486 uuids). Intersection via comm -12.",
"observed": "815 distinct superseded-draft uuids, 3,486 distinct uuids across every preservedMessages array (uuids and allUuids unioned) on all 232 boundaries. Intersection: 0.",
"rule": "Draft uuids = the uuid of every csift hit labelled user.unsent, deduplicated. Preserved uuids = the union of preservedMessages.uuids and preservedMessages.allUuids over every boundary, deduplicated. Membership counted as set intersection size.",
"note": "The claim's 0-of-772 re-measures as 0 of 815 on a grown corpus - the denominator moved, the answer did not. Note the union with allUuids makes this test strictly harder than the claim's instrument and it still returns 0. Code verified verbatim at src/model/taxonomy.rs:152-156."
}
]
},
{
"id": "CMP-008",
"area": "compaction",
"behavior": "`isVisibleInTranscriptOnly` is a DISPLAY flag carried on compaction summaries only - 232 of 232 records with the top-level key are `isCompactSummary:true` user records, and no other record type carries it - and `isMeta` is an AUTHORSHIP flag a summary does not even carry (0 of 232 summaries have the key); neither reports whether the model received a record. The compaction summary IS delivered - the conversation DAG threads through it (231 of 232 measured).",
"depends": "csift keeps the summary LLM-visible and reads neither flag as a delivery signal; misreading `isVisibleInTranscriptOnly` as a delivery flag would hide the one record carrying the compacted context from every role-level selector.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "165-170",
"snippet": " /// drew real replies before the retraction.)\n /// - `harness.compaction.boundary`: a system record with NO message field at\n /// all - pure compaction metrics. (The compaction SUMMARY is visible: the\n /// DAG threads through it; `isVisibleInTranscriptOnly` is a display flag on\n /// summaries, not a delivery flag, and `isMeta` is an authorship flag -\n /// neither is a visibility instrument.)"
}
],
"instrument": "Test the PARSED key, not the byte string: `rg -NI --no-filename 'isVisibleInTranscriptOnly' -g '*.jsonl' . | jq -r 'select(has(\"isVisibleInTranscriptOnly\"))|[.type,(.isCompactSummary|tostring)]|@tsv' | sort | uniq -c`. A bare `rg -c 'isVisibleInTranscriptOnly'` overcounts 2.75x on a corpus whose sessions discuss the format.",
"located": {
"claude_code": "2.1.231",
"csift": "0.9.4",
"source": "SPEC.md section 5; SPEC.md section 6 v0.9.4 ledger item 7; src/model/taxonomy.rs llm_visible comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && rg -NI --no-filename 'isVisibleInTranscriptOnly' -g '*.jsonl' . | jq -r 'select(has(\"isVisibleInTranscriptOnly\"))|[.type,(.isCompactSummary|tostring),(.isVisibleInTranscriptOnly|tostring)]|@tsv' | sort | uniq -c ; same stream WITHOUT the has() filter, to expose the byte-grep overcount ; rg -NI --no-filename '\"isCompactSummary\":true' -g '*.jsonl' . | jq -r 'select(.isCompactSummary==true)|[(has(\"isMeta\")|tostring),(.userType)]|@tsv' | sort | uniq -c ; Python pass building a uuid->children index per file and testing whether each summary uuid is named as some record's parentUuid",
"observed": "232 records carry a top-level isVisibleInTranscriptOnly key; all 232 are type=user with isCompactSummary=true and isVisibleInTranscriptOnly=true, and no other record type carries the key. The byte grep the claim proposes matches 638 lines, 2.75x the true 232, because csift's own dev sessions quote the field name in prose and tool results. Summaries carry no isMeta key at all (0 of 232 have it) and userType is 'external' on 232/232. DAG threading: 231 of 232 summary uuids are named as parentUuid by at least one later record in the same file (children: 211 attachment, 15 user, 7 assistant); 1 summary is unthreaded.",
"rule": "One row per raw jsonl line, corpus-wide, keyed on the PARSED top-level key (never on the raw byte string). DAG denominator = summary records in scope; a summary counts as threaded when its uuid appears as some other record's parentUuid in the same file.",
"note": "The claim holds; two corrections. First, its instrument as written is a byte grep that overcounts 638 to 232 on this corpus - the parsed form is the counting rule a stranger should rerun. Second, the DAG figure re-measures as 231/232 rather than 225/228, and the corpus shows summaries carry no isMeta key at all, so 'isMeta is an authorship flag' is a statement about other records, not about summaries. Code verified verbatim at src/model/taxonomy.rs:157-162; Class::llm_visible excludes CompactionBoundary but not CompactionSummary (src/model/taxonomy.rs:175-186)."
}
]
},
{
"id": "CMP-009",
"area": "compaction",
"behavior": "A compaction summary preserves task STATE in a nine-section synthesis whose section 6 lists the user messages, but it loses user-side TURN fidelity by VOLUME rather than by bullet count: the bullet count tracks the real turn count closely (measured 22 turns to 20 bullets, 27 to 26, 12 to 14 over three sessions), while the bullet text carries only 0.29x to 0.89x the characters of the turns it stands for. Explicit `...`-truncated bullets are the minority form: 67 of 2,089 measured bullets (3.2%), in 38 of 232 summaries. The nine numbered headers are the dominant but not the only rendering - all 232 summaries carry a user-messages section, 165 under the literal header `6. All user messages`.",
"depends": "`verbatim` exists to supplement exactly that loss: it re-emits the clipped verbatim turns in order with their `Lnnnnn` line numbers instead of re-deriving task state, and its dedup fingerprints are extracted from those bullets, so a change to the summary template moves the prefix matching.",
"code": [
{
"path": "src/turns/build.rs",
"lines": "245-251",
"snippet": "/// Extract dedup fingerprints from a summary body: the §6 \"All user messages\" bullets\n/// and the §9 verbatim last-assistant quote (the only verbatim turns a summary holds).\n/// Each fingerprint is `normalize_line(text).to_lowercase()` truncated to the first\n/// [`DEDUP_PREFIX`] chars. Conservative: when the structured sections are not found,\n/// every `- ` bullet line in the body is fingerprinted (a superset - still strict per\n/// line). Robust to summaries that omit the exact headers.\npub(crate) fn summary_fingerprints(body: &str) -> Vec<String> {"
},
{
"path": "src/turns/config.rs",
"lines": "43-47",
"snippet": "// A genuine-user turn can own a LONG run of agent messages (a debugging/build chain the\n// model narrates step by step) that a compaction summary clips to its single §9 EOT\n// quote. The reconstruction's job is to restore the LOAD-BEARING members of that run\n// without flooding the budget with pure \"let me look into this\" declarations. The model\n// keeps EVERY agent message on the slice (`TurnSlice.agents`) but SELECTS a survivor set."
}
],
"instrument": "`csift search '' @<session> -t harness.compaction.summary --no-truncate`, count the bullets in the user-messages section, and compare with the genuine-user turn count preceding that boundary (`csift stats @<session> --format json | jq .turns` over the pre-boundary window). Counting rule: summary bullets versus genuine-user turns before the boundary.",
"located": {
"claude_code": "2.1.258",
"csift": "0.4.0",
"source": "SPEC.md section 10.2; SPEC.md section 6.8"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Python pass over the 23 boundary-bearing files extracting every summary body, locating its user-messages section and counting bullet lines and ellipsis-terminated bullet lines; plus, per session, a window join of csift search '' @<session> -t user.message --format json --max-count 0 --no-truncate --no-subagents against the boundary line numbers, comparing bullet count and total bullet characters with turn count and total turn characters; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}All user messages.{0,400}' and the same for '8. Current Work:' and '9. Optional Next Step:'.",
"observed": "232 of 232 summaries carry a user-messages section, but the literal numbered headers are not universal: '6. All user messages' appears in 165/232 and '9. Optional Next Step' in 178/232 - the remaining bodies use markdown or unnumbered variants ('All user messages' 52, 'User Messages' 15), while 'Primary Request' and the closing 'Summary:' scaffold appear in 232/232. The 2.1.258 summary shows the full nine numbered sections, 1 Primary Request and Intent through 9 Optional Next Step. Truncation: of 2,089 user-message bullets corpus-wide only 67 (3.2%) end in an ellipsis, and only 38 of 232 summaries contain even one. What is actually lost is character volume, not bullet count: per compaction window, bullets vs genuine-user turns ran 11/8, 1/2, 1/2, 1/2, 6/8, 13/13, 13/14, 14/12, and total bullet characters vs total turn characters ran 0.89x, 0.38x and 0.29x on the three sessions measured. One session totalled 22 genuine-user turns to 20 bullets, the same shape as the claim's 22-to-17. The binary carries two template variants for section 6, both saying 'List ALL user messages that are not tool results' with no truncation instruction.",
"rule": "Bullets = lines matching a leading '-' or '*' inside the user-messages section of a summary body. Truncated = the bullet's text, with a trailing quote stripped, ends in '...' or a single-character ellipsis. Turns = csift hits labelled user.message whose line number falls strictly between the previous boundary line and this boundary line; characters = length of the untruncated csift excerpt.",
"note": "The claim's direction is right and its 22-to-17 figure is close to a re-measured 22-to-20, but the stated MECHANISM ('clips real prose turns to ...-truncated bullets') is the minority case at 3.2% of bullets. Since verbatim's dedup fingerprints key on those bullets, the correction matters: the bullets are mostly complete quoted messages, which is why prefix fingerprinting works at all. Code verified verbatim at src/turns/build.rs:245-251 and src/turns/config.rs:43-47."
}
]
},
{
"id": "CMP-010",
"area": "compaction",
"behavior": "The assistant side of a compaction summary collapses to AT MOST ONE verbatim quote and often to none: over 8 measured compaction windows holding 785 assistant messages, 3 windows carried exactly one 60-character-verbatim assistant quote and 5 carried zero, and no window carried two. When a quote is present it is not reliably the last pre-compaction assistant message (1 of 3 measured). Every summary ends with a trailer pointing at the full transcript path (232 of 232). CC 2.1.258 ships two section-9 templates and only one of them asks for direct quotes, which is consistent with the quote being optional.",
"depends": "That single end-of-turn quote is why `verbatim`'s agent-message selection defaults to LONGEST rather than LAST and why its richness profiles exist: everything between the first and last assistant message of a compacted turn survives only in the raw transcript.",
"code": [
{
"path": "src/turns/config.rs",
"lines": "43-47",
"snippet": "// A genuine-user turn can own a LONG run of agent messages (a debugging/build chain the\n// model narrates step by step) that a compaction summary clips to its single §9 EOT\n// quote. The reconstruction's job is to restore the LOAD-BEARING members of that run\n// without flooding the budget with pure \"let me look into this\" declarations. The model\n// keeps EVERY agent message on the slice (`TurnSlice.agents`) but SELECTS a survivor set."
}
],
"instrument": "Read one summary with `csift show @<session> --uuid <summary uuid>` and count its verbatim assistant quotes, then compare with `csift search '' @<session> -t agent.message -c` over the same pre-boundary window. Counting rule: verbatim assistant quotes in the summary versus assistant message records before the boundary.",
"located": {
"claude_code": "2.1.258",
"csift": "0.4.0",
"source": "SPEC.md section 10.2; SPEC.md section 6.8"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Python pass per session: for each compaction window (previous boundary to this boundary) collect every csift hit labelled agent.message with --no-truncate, whitespace-normalise both the summary body and each assistant text, build the set of all 60-character shingles of the summary body, and count the assistant messages having at least one 60-character run in common with the summary; also record the line of the last assistant message in the window. Plus rg -c 'read the full transcript at' over all 232 extracted summary bodies, and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Optional Next Step.{0,1400}' | sort -u.",
"observed": "8 compaction windows across 3 sessions, holding 785 assistant messages (72, 12, 8, 6, 68, 245, 191, 183). Verbatim assistant quotes found in the summary: 1, 1, 1 in three windows and 0 in the other five - never more than one in any window. Of the 3 quotes, the quoted message was the LAST assistant message of its window in 1 case; in the other 2 the quote came from an earlier message (quoted line 3941 while the last was 3993; quoted 3200 while the last was 3945). The trailer 'read the full transcript at' is present in 232 of 232 summaries, as is 'Continue the conversation from where it left off'. The 2.1.258 binary carries exactly two section-9 templates: a long one that asks only for the next step, and a short one adding 'Include direct quotes from the most recent conversation.'",
"rule": "One row per assistant-message record in the window. A record counts as quoted when its whitespace-normalised text shares at least one 60-character substring with the whitespace-normalised summary body; window = lines strictly between the previous boundary and this boundary.",
"note": "The load-bearing consequence survives and is in fact strengthened: since the surviving quote is at most one and is not reliably the LAST assistant message, verbatim's LONGEST-not-LAST default and its richness profiles are the right design. The 'about 239 assistant turns' figure is in range - one measured window held 245. Code verified verbatim at src/turns/config.rs:43-47."
}
]
},
{
"id": "CMP-011",
"area": "compaction",
"behavior": "A compaction summary is a turn MEMBER, not a turn delimiter, so a backward walk from EOF passes through compaction boundaries transparently. Measured at the default 40,000-char budget on the five most compaction-heavy top-level transcripts in the corpus, the selected window crossed 1, 4, 5, 2 and 10 boundaries against session totals of 80, 33, 32, 15 and 11 - up to 10 of a session's 11 boundaries in one 40K reconstruction. How deep the window reaches is budget-relative, not a fixed number: a session whose turns are long spends the budget before the first boundary (1 of 80), a session of short turns crosses nearly all of them (10 of 11).",
"depends": "`verbatim`'s recency-first selection reaches back across multiple compactions by default (`--max-compactions N` caps the crossing count) and banners every crossed summary; treating a summary as a delimiter would stop the reconstruction at the newest boundary.",
"code": [
{
"path": "src/turns/build.rs",
"lines": "5-9",
"snippet": "/// Build the per-turn slices + summary dedup sets from a session's line-numbered\n/// records. Turn segmentation reuses the single shared engine\n/// [`group_turn_indices_deduped`], so an esc-cancel / edit-resend draft never surfaces as a\n/// phantom turn (§6.4.1); a compaction summary is a turn MEMBER (it is excluded from\n/// genuine-user), so the walk is transparent to it."
},
{
"path": "src/turns/select.rs",
"lines": "294-301",
"snippet": "/// The EXACT compaction-boundary banner line a crossed summary renders to (no trailing\n/// newline). The renderer and the budget reservation both call this so the reserved\n/// banner length is byte-for-byte what is emitted.\npub(crate) fn boundary_banner_line(line_no: usize) -> String {\n format!(\n \"{0} compaction boundary · summary at L{1} · (turns below predate it) {0}\",\n \"══\", line_no\n )"
}
],
"instrument": "csift verbatim @<session> --budget 40000 --format json | jq -rc 'select(.kind==\"compaction_boundary\")|.line' | wc -l, cross-checked against the header row's boundaries_spanned and boundaries_total. Counting rule: boundaries the budget window crossed over the session's total. NOTE the boundary row's line key is `line`, not `line_no` (the row carries exactly kind/line/summary_chars), so the ledger's `.line_no` projection yields null on every row.",
"located": {
"claude_code": "2.1.258",
"csift": "0.4.0",
"source": "SPEC.md section 6.8"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for s in <five top-level sessions>; do csift verbatim @$s --budget 40000 --format json | python3 -c \"import sys,json; rows=[json.loads(l) for l in sys.stdin if l.strip()]; h=[r for r in rows if r['kind']=='header'][0]; print(h['boundaries_spanned'], h['boundaries_total'], h['chars_used'], sum(1 for r in rows if r['kind']=='compaction_boundary'))\"; done (then: csift verbatim @<session> --budget 40000 --max-compactions 2 --format json)",
"observed": "boundaries_spanned / boundaries_total at the default 40000-char budget, on the five most compaction-heavy top-level transcripts: 1/80, 4/33, 5/32, 2/15, 10/11. chars_used 39927-39999 in every run. The count of emitted kind==compaction_boundary rows equalled boundaries_spanned in all five runs (1,4,5,2,10). With --max-compactions 2 the 10/11 session fell to 1 spanned (turns 115 -> 119, chars_used 39983). Text banner emitted verbatim: '== compaction boundary . summary at L10077 . (turns below predate it) ==' (leading/trailing glyph is the double-bar character pair, four consecutive banners at output lines 10-13).",
"rule": "One run per session at the default budget. Boundaries crossed = the header's boundaries_spanned, cross-checked against the number of kind==compaction_boundary rows in the same stream; session total = the header's boundaries_total (equals csift stats' compactions).",
"note": "Mechanism confirmed and strong: transparency to boundaries is real and the cap flag demonstrably restricts it. The specific figure '26 boundaries on a 35-summary session' is not reproducible on the current corpus - no transcript has 35 compactions (the distribution is 80, 33, 32, 15, 11, 8, 7, 7, ...), and the deepest crossing measured at 40K was 10. Both code sites exist verbatim at the cited lines (src/turns/build.rs 5-9; src/turns/select.rs 294-301)."
}
]
},
{
"id": "CMP-012",
"area": "compaction",
"behavior": "A transcript whose FIRST TIMESTAMPED record is a `system`/`compact_boundary` was minted by COPYING another session at that compaction point: the copy preserves the origin's record uuids, timestamps, parentUuid and logicalParentUuid, rewrites sessionId, ADDS `sessionKind`, STRIPS the slug, and stamps the copying build's own `version` - so the copied records carry timestamps predating the file itself. Measured corpus-wide over 64 top-level transcripts, the first-timestamped-record type was `attachment` on 62, `compact_boundary` on exactly 1 (the single known fork, which csift is the only surface to flag) and `queue-operation` on 1 (correctly not flagged); zero false positives.",
"depends": "`list`'s clone probe walks head lines to the first timestamped record and early-exits (near-free on a normal transcript, where a handful of bookkeeping lines lead the file), setting `is_clone` and `clone_boundary_uuid`; without the detection a clone DOUBLE-COUNTS its inherited records on every spanning surface until it is scoped away.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "152-159",
"snippet": "/// The C-19 clone law: a transcript whose FIRST TIMESTAMPED record is a\n/// system/`compact_boundary` was minted by copying another session at a compaction\n/// point. Measured on a real 61-file project dir: exactly the one known fork\n/// detected, zero false positives; file-birthtime rules were REFUTED (filesystem\n/// copies and migrations move birthtimes days past the records). Walks head lines\n/// until the first record carrying a timestamp and early-exits - near-free on a\n/// normal transcript (a handful of bookkeeping lines lead the file).\npub(crate) fn clone_head_boundary(path: &Path) -> Result<Option<String>> {"
},
{
"path": "src/session/summarize.rs",
"lines": "165-176",
"snippet": " for line in mmap.split(|&b| b == b'\\n') {\n if TS.find(line).is_none() {\n continue;\n }\n if let Ok(Some(rec)) = crate::parse::parse_line(line) {\n if rec.timestamp.is_some() {\n let hit = rec.is_compact_boundary();\n return Ok(hit\n .then(|| rec.uuid.clone().unwrap_or_default())\n .filter(|u| !u.is_empty()));\n }\n }"
}
],
"instrument": "Corpus-wide rather than one project dir: for each top-level transcript take the first line carrying a `timestamp` and record its type/subtype, then cross-check `csift list --max-count 0 --format json | jq -r 'select(.is_clone==true)|[.clone_of]|@tsv'`. Counting rule: one row per top-level transcript, first timestamp-bearing record only.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger item 3; AGENTS.md section 3.5; CHANGELOG 0.9.4; csift dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 census over every top-level transcript: for each ~/.claude/projects/<dir>/*.jsonl take the first JSON line carrying a non-null `timestamp` and print type+\"/\"+(subtype or \"-\"); then csift list --max-count 0 --format json | filter is_clone==true; then a python3 field-diff of the clone's head boundary line against the origin line carrying the same record uuid.",
"observed": "64 top-level transcripts corpus-wide. First-timestamped-record type: attachment/- on 62, system/compact_boundary on 1, queue-operation/- on 1. csift list reported exactly 1 row with is_clone true, and it is the compact_boundary-headed file; the queue-operation-headed file was NOT flagged. Within the clone-bearing project dir alone (8 top-level transcripts): 7 attachment, 1 compact_boundary. Clone head boundary sits at line 28, behind 27 untimestamped bookkeeping lines (ai-title, agent-name, mode, permission-mode, atis-latch, file-history-snapshot). Copy evidence: 558 of the clone's uuid-bearing records carry a uuid that also occurs in the origin; 0 of those 558 carry a slug; the clone's first record timestamp precedes its own file birthtime by 15h19m. Head-boundary field diff vs the origin's native copy: 14 fields byte-identical (uuid, logicalParentUuid, parentUuid, timestamp, compactMetadata, content, cwd, entrypoint, gitBranch, isSidechain, level, subtype, type, userType), 4 rewritten (sessionId, slug present->absent, sessionKind absent->\"bg\", version 2.1.251->2.1.252).",
"rule": "One row per top-level transcript, first timestamp-bearing record only. Copied-record count = clone records whose own uuid also appears as some origin record's own uuid.",
"note": "The claim's '61-file project directory / attachment on 60' scope no longer exists - no project dir in the corpus holds 61 top-level transcripts (the largest holds 23), so the reproducible scope is corpus-wide (64) or the clone's own dir (8). A THIRD head shape has appeared since the measurement: one transcript's first timestamped record is a `queue-operation` line; the rule ignores it correctly, but a future ledger should stop describing the head census as two-valued. The same stale '61-file project dir' number is baked into the src/session/summarize.rs doc comment at line 154. Both code snippets exist verbatim at the cited lines (152-159 and 165-176). New untimestamped bookkeeping line types not in the ledger's enumerations were observed leading the file: `atis-latch`, `bridge-session`, `cost-state`, `file-history-delta`."
}
]
},
{
"id": "CMP-013",
"area": "compaction",
"behavior": "File birthtime is NOT a fork signal. Measured over 64 top-level transcripts, the birthtime is LATER than the first record on 64 of 64 (filesystem copies and migrations move birthtimes past the records; the largest ordinary gap is 45d17h, the median 4h13m). The true fork's gap is only 15h19m and ranks 27th of 64, so any 'first record much older than birthtime' threshold that catches the fork also fires on 26 ordinary transcripts. Comparing a UTC record timestamp against a LOCAL birthtime additionally inflates the delta by the zone offset - the same 15h19m gap reads as 25h19m.",
"depends": "csift's clone law calls no `stat()`, reads no birthtime and trusts no clock, so it never reports an ordinary migrated transcript as a fork; the structural head-record rule is the whole instrument.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "155-158",
"snippet": "/// detected, zero false positives; file-birthtime rules were REFUTED (filesystem\n/// copies and migrations move birthtimes days past the records). Walks head lines\n/// until the first record carrying a timestamp and early-exits - near-free on a\n/// normal transcript (a handful of bookkeeping lines lead the file)."
}
],
"instrument": "For each top-level transcript compute first_record_ts_utc - birthtime_utc with BOTH operands in UTC, sort descending, and locate the known fork's rank. Counting rule: one delta per file; the refutation number is the count of non-forked files whose delta exceeds the fork's (26 of 63 here), which is threshold-free.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "AGENTS.md section 3.5; CHANGELOG 0.9.4; csift dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over all 64 top-level transcripts: delta = file birthtime (os.stat st_birthtime, converted to UTC) minus the first record timestamp; separately the mixed form, naive-local birthtime minus the naive UTC record timestamp; then rank the one true clone within the descending delta list.",
"observed": "64 of 64 transcripts have a POSITIVE delta (birthtime later than the first record). Threshold sweep: delta > 1h on 40 files, > 6h on 31, > 24h on 26, > 72h on 21. Largest deltas: 45d17h, 27d11h, 20d23h, 20d7h, 19d12h. Median delta 4h13m. The one true clone's correct delta is 15h19m and it ranks 27th of 64 by descending delta - 26 ordinary non-forked transcripts have a LARGER delta than the fork. The same clone measured with a naive-local birthtime against the UTC record timestamp reads 1d1h19m (25h19m), an inflation of exactly 10h, the local zone offset.",
"rule": "One delta per top-level transcript; both operands converted to UTC before subtracting. Discriminating power = how many non-clones outrank the clone at its own delta (a threshold low enough to catch the clone also fires on all of them).",
"note": "The claim's '50 of 61' is threshold-dependent and no longer reproduces as stated (at >1h the current corpus gives 40 of 64; at >24h, 26 of 64). The rank-based rule above is threshold-free and stronger: the fork sits in the middle of the ordinary distribution, so birthtime cannot separate it at any threshold. The 15h19m -> 25h19m zone-offset inflation reproduced exactly. Code snippet exists verbatim at src/session/summarize.rs 155-158, and the function calls no stat(): it mmaps and reads head lines only (verified by reading the whole function body)."
}
]
},
{
"id": "CMP-014",
"area": "compaction",
"behavior": "A clone's head `compact_boundary` is a RE-SERIALIZED copy of the origin's, not a byte copy: the record's identity fields survive verbatim (the same boundary `uuid`, `logicalParentUuid`, `parentUuid`, `timestamp`, `compactMetadata` and `content`) while four fields are rewritten at copy time (`sessionId` to the clone's own, `slug` stripped, `sessionKind` added, `version` stamped with the COPYING build - 2.1.252 against the origin's 2.1.251). Because the uuid survives, sweeping the project directory for that uuid returns exactly two files - the clone and its origin - even when the sweep is recursive over 747 subagent transcripts.",
"depends": "csift's origin join demands the uuid appear as a record's OWN `uuid` on a record that is itself a `compact_boundary`, so a prose mention of the uuid or a `logicalParentUuid` back-reference is refused and a co-clone (whose head probe returns the same uuid) is skipped; the one memmem sweep over the directory's siblings is paid only when a clone was detected.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "181-189",
"snippet": "/// Join a detected clone to its ORIGIN: the sibling transcript where the boundary\n/// record NATIVELY lives. A prose mention of the uuid parses to a record whose own\n/// uuid differs; a co-clone's head probe returns the same boundary uuid and is\n/// skipped. Cost (one memmem sweep over the project dir's siblings) is paid ONLY\n/// when a clone was detected.\npub(crate) fn clone_origin(path: &Path, boundary_uuid: &str) -> Option<String> {\n let dir = path.parent()?;\n let finder = memchr::memmem::Finder::new(boundary_uuid.as_bytes());\n let entries = std::fs::read_dir(dir).ok()?;"
}
],
"instrument": "`csift list <project dir> --format json | jq -r 'select(.is_clone==true)|[.session_id,.clone_of,.clone_boundary_uuid]|@tsv'`, then `grep -rl <that boundary uuid> <project dir>` must return exactly two `.jsonl` files. Counting rule: one file per `grep -l` line.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "CHANGELOG 0.9.4; src/session/summarize.rs clone_origin comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects/<project-dir> && rg -l --max-depth 1 '<boundary uuid>' . ; rg -l '<boundary uuid>' . | wc -l (the second form is recursive over the whole project dir, subagent trees included); plus a python3 pass listing every origin record whose OWN uuid equals the clone's head boundary uuid, and a field-by-field diff of the two boundary lines.",
"observed": "Non-recursive sweep returned exactly 2 .jsonl files (the clone and its origin). The recursive sweep over the same project dir - 755 .jsonl files including 747 subagent transcripts - returned the SAME 2 files, count 2. Exactly one origin record carries that uuid as its OWN uuid: a system/compact_boundary at origin line 90946 with an identical timestamp. The clone's logicalParentUuid is byte-identical to the origin boundary's, and it resolves natively to an origin attachment record at line 90931. Field diff of the two boundary lines: 14 fields identical, 4 rewritten (sessionId, slug present->absent, sessionKind absent->\"bg\", version 2.1.251->2.1.252).",
"rule": "One file per `rg -l` line, run twice (top level only, then recursive) over the one project dir holding the clone.",
"note": "The load-bearing part - the uuid-join to exactly one origin file - reproduced exactly, including the same logicalParentUuid. Only the phrase 'byte-copy' needed correcting; the 4 rewritten fields are what a stranger would trip on if they diffed the two lines expecting equality. The differing `version` field is independent evidence that the copy is written fresh by the forking build rather than memcpy'd. Code snippet exists verbatim at src/session/summarize.rs 181-189, and the join predicate in the function body does require the parsed record's OWN uuid to match AND to be a compact_boundary."
}
]
},
{
"id": "CMP-015",
"area": "compaction",
"behavior": "A session cloned at a compaction carries NO `plan_mode` attachment of its own - the attachment history predates the copy point and is not carried over - and the slug is STRIPPED from every copied record (0 of 558 copied records carry one; the clone's eventual slug differs from the origin's). The first slug-carrying record in such a fork is therefore a NATIVE record, and here it is the fork's own first `compact_boundary` (line 1471): a compaction mints a slug even when Plan Mode never ran.",
"depends": "Claude Code's own plan binding rule takes the first slug-carrying record in the log and consults no attachment, so csift's `plan` falls back to that law with `binding_source:\"slug-only\"` plus a `minted_at_compaction` flag; binding by attachment alone answered `no Plan Mode` for forked sessions whose plan file Claude Code will still inject or rebuild in full.",
"code": [
{
"path": "src/plan.rs",
"lines": "137-144",
"snippet": " // No `plan_mode` anywhere: fall back to Claude Code's ACTUAL binding law - the\n // FIRST record carrying a `slug` binds `<plans-dir>/<slug>.md` (the harness's\n // getSlugFromLog takes the first slug in the log, no attachment consulted). A\n // forked/background session reaches this state by construction (the clone strips\n // attachment history), and its slug is often minted by the first own compaction -\n // reporting \"no plan\" there is a WRONG answer: CC will inject/rebuild that file.\n if latest.is_none() {\n if let Some((line_no, rec)) = first_slug_record(bytes) {"
}
],
"instrument": "`csift plan @<a background-forked session>` must return a slug-bound plan file with `binding_source` `slug-only` and `minted_at_compaction` true. Counting rule: the first record carrying a `slug` key, scanned forward, one per file.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "AGENTS.md section 3.5; src/plan.rs binding comments"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift plan @<clone> --no-subagents --format json ; rg -c 'plan_mode' <clone>.jsonl <origin>.jsonl ; a python3 pass printing every clone line containing the token plan_mode with its attachment.type ; a python3 slug census of both files (distinct slug values, first slug-carrying record); strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'getSlugFromLog'.",
"observed": "csift plan on the clone returns exactly one top-level row: binding_source \"slug-only\", minted_at_compaction true, line 1471, plan_exists true, plan_file ~/.claude/plans/<slug-A>.md. The clone matches the token plan_mode on 4 lines, but every one is a `plan_file_reference` attachment whose CONTENT quotes the words 'plan_mode attachment' in prose - there is no attachment of type plan_mode (the origin has 75 such lines). Slug census: clone has exactly 1 distinct slug on 2901 records, first carrier line 1471, a NATIVE system/compact_boundary (its uuid is absent from the origin); origin has exactly 1 distinct slug, a DIFFERENT value, on 69933 records, first carrier an assistant record at line 1575. Of the clone's 558 copied records (uuid also in the origin), 0 carry a slug. Binary: `function j(n){let e=n.messages.find((i)=>i.slug)?.slug;if(e===void 0)return;if(!ue.test(e)){t(\\`getSlugFromLog: rejecting malformed transcript slug (${e.length} chars)\\`);return}return e}` and the plan path builder `case\"plan\":return r(t,\"plans\",\\`${n.name}.md\\`)`.",
"rule": "First record carrying a `slug` key, scanned forward, one per file; plan_mode presence counted as attachment records whose attachment.type equals plan_mode, not as raw token hits.",
"note": "The clause 'slug-carrying records survive the copy' is REFUTED by the instrument: zero of the 558 copied records carry a slug, and the clone's single slug value is not the origin's. What survives the copy is the uuid, not the slug. Everything else holds, and the binding law is now confirmed at binary level: getSlugFromLog takes the FIRST message carrying a slug, validates it against a lowercase/dash/length pattern, consults no attachment, and the plan path is built as <plans-dir>/<slug>.md. Corroborating detail: the clone's copied `plan_file_reference` attachment still points at the ORIGIN's plan file while the fork's own binding names a different file, so a surface that bound on that attachment would answer with the wrong plan. Second code site (src/plan.rs 68-76) exists verbatim; the FIRST code site had drifted by one line (claim said 134-141, actual 135-142) - corrected above."
}
]
},
{
"id": "CMP-016",
"area": "compaction",
"behavior": "`sessionKind` is a PER-RECORD, write-time stamp of the writing process's kind. The harness's value domain is three-valued - `bg`, `daemon` and `daemon-worker` (the binary gates on exactly those three, and reads the field back out of a transcript line's head) - of which only `bg` occurs on disk here, on 909/909 stamped records. It rides `attachment`/`assistant`/`user`/`system` records while a background job is the writer and stops entirely once the same file is resumed interactively; the bookkeeping line types never carry it at all (measured never-stamped: last-prompt, ai-title, agent-name, mode, permission-mode, file-history-snapshot, file-history-delta, queue-operation, and the newer atis-latch, bridge-session and cost-state).",
"depends": "csift's clone probe deliberately does NOT key on `sessionKind` - a file containing it is not necessarily a clone and a record lacking it is not necessarily native - the first-timestamped-record boundary shape is the discriminator; switching would make `list` report false clones.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "152-158",
"snippet": "/// The C-19 clone law: a transcript whose FIRST TIMESTAMPED record is a\n/// system/`compact_boundary` was minted by copying another session at a compaction\n/// point. Measured on a real 61-file project dir: exactly the one known fork\n/// detected, zero false positives; file-birthtime rules were REFUTED (filesystem\n/// copies and migrations move birthtimes days past the records). Walks head lines\n/// until the first record carrying a timestamp and early-exits - near-free on a\n/// normal transcript (a handful of bookkeeping lines lead the file)."
}
],
"instrument": "Per project dir: `rg -l '\"sessionKind\"' ./<project-dir>/ | grep -c '\\.jsonl$'` (a corpus-wide rg over-reports by counting externalised tool-result .txt files that merely discuss the field). Then inside a stamped transcript tabulate presence of the top-level key by `type`, and split the records by whether their uuid also occurs in the origin. Counting rule: one row per jsonl line. Measured: 21 transcripts corpus-wide (one session plus its 20 subagent transcripts); within that file, 558/558 copied records stamped, then a mixed band, then 4467/4467 unstamped after the interactive-resume gap at line 1361.",
"located": {
"claude_code": "2.1.252",
"csift": null,
"source": "csift dev session 2026-09-01"
},
"first_seen_claude_code": "2.1.252",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && for d in */; do echo \"$(rg -l --no-messages '\\\"sessionKind\\\"' \"./$d\" | wc -l) / $(find \"./$d\" -name '*.jsonl' | wc -l) $d\"; done (scoped one project dir per invocation); then inside the one stamped top-level transcript a python3 census of sessionKind presence by record `type`, by copied-vs-native uuid, and by line band; then strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'sessionKind'.",
"observed": "26 files corpus-wide contain the literal key, in 2 project dirs (22 + 4); of these, 4 are externalised tool-result .txt files and 1 is a .txt, leaving exactly 21 TRANSCRIPTS: 1 top-level (the fork) + 20 of its subagent transcripts, all in one project dir. Inside the fork: 909 stamped records, value `bg` on 909/909 (single value). Stamped by type: attachment 406, assistant 268, user 152, system 83 - and no other type ever stamped. Never stamped: ai-title 261, agent-name 261, mode 261, permission-mode 261, last-prompt 261, atis-latch 261, bridge-session 206, file-history-snapshot 75, queue-operation 84, file-history-delta 25, cost-state 4. Band: first stamped record at line 28 (the head boundary), last at line 1361 (2026-09-01T09:42:17.577Z); the next timestamped record is 13m15s later at line 1367 and every one of the 4467 records after line 1361 is unstamped. Copied-vs-native split: 558/558 copied records stamped, 0 unstamped; native records 351 stamped, 2959 unstamped. Binary: `let a=G1(t,\"sessionKind\");return a===\"daemon\"||a===\"daemon-worker\"}`, `B=G1(F,\"sessionKind\"),U=B===\"bg\"||B===\"daemon\"||B===\"daemon-worker\"?B:void 0`, `...t.sessionKind===\"bg\"?[\"bg\"]:[]`, and `filtered from /resume: sessionKind=`.",
"rule": "File count: one row per file matched by a per-project-dir `rg -l`, then transcripts separated from tool-result .txt by extension. Record census: one row per JSON line, keyed on presence of the top-level `sessionKind` key.",
"note": "The stated instrument does NOT work: `csift search '\"sessionKind\"' -l` matches rendered record TEXT, not raw JSON keys, so it returned 12 records in 12 transcripts that merely mention the field in prose - none of them stamped files. Use a scoped rg (or a jq/python key probe) for field-presence questions. The 21-transcript count and the 558/558 copied-record figure reproduced EXACTLY; the trailing unstamped band is now 4467 records (was 834) because the file kept growing after the interactive resume. New corpus fact: the binary's value domain is wider than the disk observation, so 'single observed value bg' should be stated as 'single observed value, of three the harness can write'. The independence claim also verified: 21 transcripts carry sessionKind while csift's list reports exactly 1 clone, so the field is not a clone discriminator - the clone probe reads only the first timestamped record's shape."
}
]
},
{
"id": "CMP-017",
"area": "compaction",
"behavior": "A compaction re-anchor can REPLAY duplicate records: the same `uuid` values and timestamps appear twice under different `parentUuid`s, and the replayed copy carries its top-level usage ZEROED (`input_tokens` 0, `output_tokens` 0, cache fields 0) while the real numbers survive only inside `usage.iterations` - so usage is not always identical within one `message.id`.",
"depends": "csift's token dedupe takes a per-field MAX across an id's records rather than first-wins: identical on clean data, immune to the zeroed replay, and commutative so the rayon fold stays correct; first-wins would make the reported total depend on traversal order.",
"code": [
{
"path": "src/stats.rs",
"lines": "250-256",
"snippet": " // CC repeats the IDENTICAL message.usage on every per-block record of\n // one API message; summing per record over-reports 2.2-3.5x (measured).\n // Dedupe per FILE by message.id, taking the per-field MAX across the\n // id's admitted records: identical on clean data, and immune to the\n // compaction-replay shape where a replayed copy carries ZEROED usage\n // (first-wins would depend on traversal order). An id-less record\n // counts on its own, as before."
}
],
"instrument": "Group a transcript's records by `message.id` and flag ids whose members disagree on any usage field; count duplicate uuids per file alongside. Counting rule: per id, per file. Measured over the 6 largest top-level transcripts: 4 had 0 mismatching ids, 1 had exactly 1 (a duplicate-uuid replay whose second copy is all-zero at the top level while usage.iterations keeps the real numbers), and 1 had exactly 1 of a different shape (a mid-stream partial, output_tokens 2 -> 1404, in a file with no duplicate uuids). Duplicate uuids themselves are common (474, 464, 250, 1175 in four of the six files), so the replay is frequent and the ZEROED replay is the rare variant.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.2",
"source": "src/stats.rs usage-dedupe comment; csift dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over the 6 largest top-level transcripts (>5MB, sorted by size): group every record with a message.id by that id, collect the 4-tuple (input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens), flag ids whose members disagree, and separately count uuids occurring more than once per file; then dump the disagreeing records' uuid, parentUuid, timestamp, top-level usage and usage.iterations.",
"observed": "6 transcripts scanned (687MB/5385 ids, 396MB/7538, 284MB/12691, 113MB/6208, 94MB/2906, 94MB/2906). Mismatching ids: 0, 1, 0, 1, 0, 0. Duplicate uuids per file: 474, 464, 250, 0, 1175, 1175 - so replayed records are common while usage disagreement is rare. The one zeroed replay: the SAME three uuids appear at lines 27860-27862 and again at 29136-29138 with byte-identical timestamps; the head of the replayed triple has a DIFFERENT parentUuid; the first copy carries top-level usage (2, 708, 474834, 3997) on all three records and the replayed copy carries (0, 0, 0, 0) on all three, while `usage.iterations[0]` on the zeroed copies still holds {input_tokens 2, output_tokens 708, cache_read_input_tokens 474834, cache_creation_input_tokens 3997}. The other mismatching id is a DIFFERENT shape: three consecutive records at lines 5470-5472 in a file with 0 duplicate uuids, output_tokens 2 then 1404 then 1404 with the other three fields equal - a mid-stream partial, not a replay.",
"rule": "Per message.id, per file: an id is 'mismatching' when its member records disagree on any of the four usage fields. Duplicate uuids counted per file as uuids occurring on more than one line.",
"note": "The behavior sentence reproduced verbatim, down to the shape of the record triple; only the sample counts needed correcting (claim said 3 mismatching ids in one large session, measured 1). The second shape found here strengthens rather than weakens the design rationale: a mid-stream partial breaks first-wins in the OPPOSITE direction (first-wins would under-count 1402 output tokens), so per-field MAX is the only rule that survives both shapes. Code snippet exists verbatim at src/stats.rs 250-256."
}
]
},
{
"id": "CMP-018",
"area": "compaction",
"behavior": "A compaction does NOT rewrite the transcript: Claude Code APPENDS a `type:\"system\"` `subtype:\"compact_boundary\"` record and then, on the VERY NEXT line, a `type:\"user\"` `isCompactSummary:true` `isVisibleInTranscriptOnly:true` record, so every pre-compaction line number stays valid and the pre-compaction records stay in the same file below the boundary. The boundary is the append point and the summary is always boundary+1 (13 of 13 events measured); the transcript writer is an append call (`appendEntryToFileAsync`), never a rewrite of earlier lines.",
"depends": "The whole `Lnnnn` addressing contract rests on it: `verbatim` reconstructs the clipped turns out of the SAME file (its crossed-summary banner names the summary's own line and says the turns below predate it), a `csift show --line` refetch printed before a compaction still resolves the identical record afterwards, and `image`'s stable id `L<line>i<n>` is stable for exactly this reason. If compaction rewrote or renumbered the file, every stored line address in a prior answer would go stale.",
"code": [
{
"path": "src/image.rs",
"lines": "8-11",
"snippet": "//! Stable image id = `L<line>i<n>`: the 1-based JSONL line of the carrying record plus the\n//! 1-based ordinal of the image among that record's image blocks. It is stable because the\n//! transcript is append-only, and it is consistent with the `Lnnnnn` line references used\n//! across `recover` / `turns` / `search` (so an id surfaced there feeds straight back here)."
},
{
"path": "src/turns/build.rs",
"lines": "5-9",
"snippet": "/// Build the per-turn slices + summary dedup sets from a session's line-numbered\n/// records. Turn segmentation reuses the single shared engine\n/// [`group_turn_indices_deduped`], so an esc-cancel / edit-resend draft never surfaces as a\n/// phantom turn (§6.4.1); a compaction summary is a turn MEMBER (it is excluded from\n/// genuine-user), so the walk is transparent to it."
},
{
"path": "src/turns/select.rs",
"lines": "294-301",
"snippet": "/// The EXACT compaction-boundary banner line a crossed summary renders to (no trailing\n/// newline). The renderer and the budget reservation both call this so the reserved\n/// banner length is byte-for-byte what is emitted.\npub(crate) fn boundary_banner_line(line_no: usize) -> String {\n format!(\n \"{0} compaction boundary · summary at L{1} · (turns below predate it) {0}\",\n \"══\", line_no\n )"
}
],
"instrument": "Note a pre-compaction record's `Lnnnn` from any csift surface, let the session compact, then re-run `csift show @<id> --line <that same line> --format json`: it must return the identical record (same `uuid`). Then `csift search '' @<id> -t harness.compaction.boundary --format json | jq -r '.hits[].line'` must be greater than every pre-compaction line, and the summary's line must be exactly one greater than the boundary's. NOTE the jq path: search JSON nests matches inside `{\"hits\":[...]}` exchange rows, so a top-level `.line` selects nothing. Counting rule: one boundary record plus one immediately following summary record appended per compaction event; line numbers are 1-based and per file.",
"located": {
"claude_code": null,
"csift": "0.9.4",
"source": "AGENTS.md section 3.5; src/image.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc258.strings.txt ; rg -o '.{0,90}subtype:\"compact_boundary\".{0,150}' cc258.strings.txt ; rg -o '.{0,140}isCompactSummary:!0.{0,120}' cc258.strings.txt ; rg -o 'appendEntryToFile[A-Za-z]*' cc258.strings.txt | sort | uniq -c -- (2) structural scan of one transcript, run once per file: python3 -c \"import json,sys\\nb=[];s=[];t=[]\\nfor i,l in enumerate(open(sys.argv[1],errors='replace'),1):\\n if not l.startswith('{'): continue\\n try: o=json.loads(l)\\n except Exception: continue\\n ts=o.get('timestamp')\\n if isinstance(ts,str): t.append((i,ts))\\n if o.get('type')=='system' and o.get('subtype')=='compact_boundary': b.append((i,ts))\\n if o.get('type')=='user' and o.get('isCompactSummary'): s.append(i)\\nprint('boundary_lines',[x for x,_ in b]); print('summary_lines',s)\\nprint('summary_minus_boundary',[y-x for (x,_),y in zip(b,s)])\\nprint('below_boundary_with_LATER_ts',[sum(1 for (l,ts) in t if l<bl and ts>bts) for bl,bts in b])\\nprint('timestamped_records',len(t))\" ~/.claude/projects/<encoded-project-dir>/<session>.jsonl -- (3) the before/after line-address test: csift search 'csift show @[0-9a-f]{8}[^ ]* --line [0-9]+' . --no-subagents -t agent.tool.use --format json (finds line addresses that were issued and answered INSIDE a transcript at a known timestamp), then re-run each recovered address today: csift show @<self> --line 37..38 --format json and csift show @<self> --line 23676 -- (4) csift search '' @<session> --no-subagents -t harness.compaction.boundary --format json | jq -r '.hits[].line' (and -t harness.compaction.summary) -- (5) csift verbatim @<session> --budget 60000 | rg 'compaction boundary' -- (6) csift search '' @<self> --no-subagents --count-by label | rg thinking",
"observed": "BINARY 2.1.258 -- the boundary constructor is verbatim present: 'function Sce(e,n,r,o,d){return{type:\"system\",subtype:\"compact_boundary\",content:\"Conversation compacted\",isMeta:!1,timestamp:new Date().toISOString(),uuid:oT(),level:\"info\",compactMetadata:{trigger:e,preTokens:n,userContext:o,messagesSummarized:d'; the summary constructor is verbatim present: 'isCompactSummary:!0,isVisibleInTranscriptOnly:!0'; the post-compaction message array is assembled boundary-first, summary-second: 'nt=mh([xt,...Tn,...rt,...ht]); xt.compactMetadata.postTokens=nt' with xt the boundary and Tn=[{...isCompactSummary:!0...}]; the transcript writer is an append: 'appendEntryToFileAsync' occurs 4x, with the error string 'appendEntryToFileAsync: append to ${e} failed: '. ON DISK -- 13 compaction events scanned in full across the two largest compacted transcripts of one project dir: boundary lines [8351,14605,19195,25509,30873,35947,40702,44001] and [4806,6196,7022,8035,11968]; summary_minus_boundary = 1 for 13 of 13 events; below_boundary_with_LATER_ts = [0,0,0,0,0,0,0,0] and [0,1,0,1,0], and the two exceptions are each the single line at B-1 carrying a timestamp only +0.199 s / +0.132 s later than the boundary (ordinary async flush of the record written just before). BEFORE/AFTER LINE ADDRESS -- a command recorded in a transcript at 2026-08-31T13:11:20Z had asked that same transcript for '--line 37..38 --format json' and its recorded answer was '37 agent.thinking' / '38 agent.thinking.narration'; re-running the identical command today returns '37 agent.thinking 2026-08-12T00:58:27.789Z' / '38 agent.thinking.narration 2026-08-12T00:58:30.303Z' -- identical, with 3 compaction events (2026-09-01T12:37:33Z, 2026-09-01T15:33:45Z, 2026-09-02T03:37:54Z) landing in between. Second receipt: at 2026-08-27T12:09:09Z the same transcript answered 'csift show ... --line 23676' with 't103 2026-08-22 21:19:43.414 AEST(UTC+10) / agent.tool.use > agent.tool.result Bash L23676 Bash {\"command\":\"SP=...'; re-running it today returns the same turn t103, the same turn timestamp 2026-08-22 21:19:43.414 AEST(UTC+10), the same label pair, the same L23676 and the same command head -- with 4 compaction events in between. The same transcript's label census over that window only grew: 1274 -> 2094 agent.thinking and 303 -> 493 agent.thinking.narration. CROSS-CHECK -- csift search -t harness.compaction.boundary returns lines [4806,6196,7022,8035,11968] and -t harness.compaction.summary returns [4807,6197,7023,8036,11969], matching the independent raw scan exactly; csift verbatim on that transcript prints five banners naming L4807, L6197, L7023, L8036, L11969 ('== compaction boundary . summary at L4807 . (turns below predate it) ==').",
"rule": "One compaction event = exactly one line with type=\"system\" and subtype=\"compact_boundary\" plus the line immediately after it with type=\"user\" and isCompactSummary truthy; line numbers are 1-based and per file. 13 events counted = every event in the two transcripts that csift stats reports with the highest compaction counts in one project dir (8 and 5; the dir holds 17 events across 5 transcripts, the remaining 4 were not scanned). 'below_boundary_with_LATER_ts' counts, for each boundary at line B with timestamp T, how many timestamped records at a line < B carry a timestamp string greater than T -- a rewrite or renumber would put many post-compaction records below B, so this count is the append test; 0 is the pass. The before/after test counts compaction events whose boundary timestamp falls strictly between the timestamp of a line address recorded inside a transcript and the moment that same address is re-fetched; a pass requires the re-fetched record to be byte-identical in turn number, timestamp, label and text head.",
"note": "Holds, with three wording/number refinements and three forward-looking cautions. Refinements: (a) the ordering is not merely 'summary greater still' -- the summary is exactly boundary+1 in 13 of 13 measured events, and the binary assembles the post-compaction array boundary-first; (b) the claim's instrument line uses `jq -r .line`, which selects nothing against the real envelope (matches live under `.hits[]`); (c) `compactMetadata` in 2.1.258 carries more than the four fields the ledger's prose names -- the constructor writes `{trigger, preTokens, userContext, messagesSummarized}` and `postTokens` is patched in afterwards, and other call sites add `preservedMessages` / `preservedSegment` (with `headUuid`/`anchorUuid`/`tailUuid`), so a consumer must not assume a closed field set. Cautions, none of which refute the claim: (1) 2.1.258 carries a second, distinct system subtype `microcompact_boundary`, which the transcript renderer explicitly returns null for; zero records with that subtype exist anywhere in the scanned project dir, so its on-disk shape is untested here and csift does not model it -- a session that micro-compacts would decide it. (2) 2.1.258 also has a `content-replacement` line type written through `insertContentReplacement` -> `appendEntry`, i.e. even a logical replacement of earlier content is expressed as a NEW appended line rather than an edit of an old one, which is consistent with the claim; zero such lines exist in the scanned corpus. (3) The append-only guarantee is per file: a session forked or cloned at a compaction is minted as a SEPARATE transcript, so a stored `Lnnnn` stays valid in the file it was read from but says nothing about the clone. Both before/after receipts come from the transcript this verification ran inside, which is precisely what makes them usable: the addresses were issued and answered by earlier work in that file, days and several compactions before the re-fetch. All three cited csift code sites were opened and match verbatim at the stated paths and lines, so corrections.code is empty: src/image.rs lines 8-11 (the `L<line>i<n>` stable-id doc naming the append-only transcript), src/turns/build.rs lines 5-9 (the compaction summary as a turn MEMBER), and src/turns/select.rs lines 294-301 (`boundary_banner_line`), whose format string emits exactly the banner observed live."
}
]
},
{
"id": "ELI-001",
"area": "elicitation",
"behavior": "An AskUserQuestion tool_use carries input.questions as an ORDERED array of {question, header, multiSelect, options[]}, and the answering carrier's toolUseResult echoes it; an option is {label, description, preview?} - the OPTIONAL third field `preview` is present on 158/416 ask-side and 155/378 echoed option objects (38-41%) and is operator-visible content (2.1.258: 'Optional preview content rendered when this option is focused. Use for mockups, code snippets, or visual comparisons'), not decoration.",
"depends": "csift's auq_exchange reads only label + description, so the per-option `preview` is dropped from every reconstructed AUQ unit; the rendered header is `[AskUserQuestion . N question(s)]` (a middle-dot separator, singular for N==1), not `[AskUserQuestion - N questions]`.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "49-53",
"snippet": " let header = q.get(\"header\").and_then(serde_json::Value::as_str);\n let question = q\n .get(\"question\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default();"
},
{
"path": "src/model/exchange.rs",
"lines": "63-70",
"snippet": " let label =\n o.get(\"label\").and_then(serde_json::Value::as_str)?;\n let desc = o\n .get(\"description\")\n .and_then(serde_json::Value::as_str)\n .filter(|s| !s.is_empty())\n .map(str::to_string);\n Some((label.to_string(), desc))"
},
{
"path": "src/model/tests/boundaries.rs",
"lines": "217",
"snippet": "\"questions\":[{\"header\":\"Routing\",\"multiSelect\":false,\"options\":[{\"description\":\"the inbound path\",\"label\":\"Route A\"},{\"description\":\"the outbound path\",\"label\":\"Route B\"}],\"question\":\"which route for the queue?\"}]"
}
],
"instrument": "`csift search \"\" <target> -t user.answer --format json | jq -r .hits[].line`, then `csift show <target> --line <n> --raw | jq '.toolUseResult.questions[0] | keys'`: the keys must be `header`, `multiSelect`, `options`, `question`, and each option object must carry `label` + `description`. Counting rule: keys of ONE question object.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 4.4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<count key sets of toolUseResult.questions[] and of every questions[].options[] object>' AND csift search 'AskUserQuestion' -t agent.tool.use --raw --max-count 0 | python3 -c '<same census on the ask side input.questions>' AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Optional preview content rendered when this option is focused'",
"observed": "Answer side: 95 answered carriers, 118 question objects, key set ('header','multiSelect','options','question') on 118/118; 378 option objects, of which ('description','label') 223 and ('description','label','preview') 155. Ask side: 111 distinct AskUserQuestion tool_use blocks, 106 with a parseable input (5 carry only '__unparsedToolInput'), 129 question objects with the same 4 keys, 416 option objects, ('description','label') 258 and ('description','label','preview') 158. Binary 2.1.258 carries the option-field descriptions 'The display text for this option that the user will see and select.', 'Explanation of what this option means or what will happen if chosen.' and 'Optional preview content rendered when this option is focused.'",
"rule": "one key set per question object and per option object, over every record csift labels user.answer corpus-wide (deduped by record uuid; --raw already deduped 116 hits to 95 distinct records, the 21 duplicates being clone-copied transcripts)",
"note": "Code sites verify verbatim: src/model/exchange.rs:49-53 and 63-70 are unchanged, and the fixture line src/model/tests/boundaries.rs:217 still carries the quoted questions[] shape. The structural claim is exactly right; only the option-object field list needed widening. csift also ignores annotations[q].preview (16 of 46 annotation objects in the corpus are {preview} only, 1 is {notes,preview}), so an operator's preview-based choice is invisible in the reconstructed unit."
}
]
},
{
"id": "ELI-002",
"area": "elicitation",
"behavior": "The answered-AskUserQuestion carrier's toolUseResult has TWO always-present keys - questions[] (the echoed ordered array) and answers{} (keyed by the VERBATIM question string) - plus THREE conditional ones: annotations{} (per-question {notes?, preview?}; absent on 1 of 95 corpus carriers), response (the operator's freeform text when they typed instead of selecting; new in the 2.1.258 schema, not yet seen on disk here) and afkTimeoutMs (set only when the dialog auto-resolved on idle; seen on 3 corpus carriers). chosenOption / answer / selected still do not exist.",
"depends": "The inline synthesized tool_result string is not TRUNCATED - it is lossy by omission: it drops every option description (0 of 378 retained) and every non-chosen label (66 of 378 retained), and it skips any question that has neither an answer nor notes.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "26-31",
"snippet": " let tur = self.tool_use_result_value();\n let answers = tur\n .as_ref()\n .and_then(|t| t.get(\"answers\"))\n .and_then(serde_json::Value::as_object)\n .filter(|m| !m.is_empty());"
},
{
"path": "src/model/exchange.rs",
"lines": "37-43",
"snippet": " // `annotations` map (§4.4) - per-question `{notes?, preview?}`; when the answer\n // is the `\"(notes only)\"` placeholder the user's ENTIRE real message lives here.\n let annotations = tur\n .as_ref()\n .and_then(|t| t.get(\"annotations\"))\n .and_then(serde_json::Value::as_object)\n .filter(|m| !m.is_empty());"
},
{
"path": "src/model/tests/boundaries.rs",
"lines": "217",
"snippet": "\"answers\":{\"which route for the queue?\":\"(notes only)\"},\"annotations\":{\"which route for the queue?\":{\"notes\":"
}
],
"instrument": "`csift search \"\" <target> -t user.answer --format json | jq -r .hits[].line` then `csift show <target> --line <n> --raw | jq '.toolUseResult | keys'`: the keys must be `answers` / `questions` / `annotations`. Counting rule: keys of ONE record's toolUseResult object.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "SKILL.md wrong-assumption table (AUQ field-name rows); src/model/exchange.rs auq_exchange doc; SPEC.md section 4.4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<count sorted(toolUseResult.keys()) per record; count option labels/descriptions that also occur in the inline tool_result string>' AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Freeform text the user typed instead of selecting a structured option'",
"observed": "toolUseResult key sets over 95 answered carriers: ('annotations','answers','questions') 94, ('answers','questions') 1. Zero records carry chosenOption, answer or selected as a key. The 2.1.258 result schema is questions + answers + response:i().optional().describe(\"Freeform text the user typed instead of selecting a structured option\") + annotations + afkTimeoutMs:A().int().positive().optional().describe(\"Set when the dialog auto-resolved after this many milliseconds of idle (user away from keyboard). Absent on every human-resolved path.\"), and the builder emits `{questions:r,answers:o,...f?.trim()&&{response:f},...d&&{annotations:d},..._&&{afkTimeoutMs:_}}`. Three on-disk carriers already show the key set ('afkTimeoutMs','annotations','answers','questions'). Of 378 echoed option objects, 66 labels and 0 descriptions occur anywhere in the inline synthesized string.",
"rule": "one key set per record's toolUseResult object; label/description retention counted as substring presence in the same record's tool_result content string, one count per option object",
"note": "Code sites verify verbatim at src/model/exchange.rs:26-31 and 37-43. The new `response` key matters for csift: auq_exchange reads answers + annotations.notes only, so on a 2.1.258 carrier where the operator typed freeform text the prose would live in toolUseResult.response and be dropped (the synthesized string would read 'The user responded: ...'); no such record exists in this corpus yet, so the gap is prospective, not observed."
}
]
},
{
"id": "ELI-003",
"area": "elicitation",
"behavior": "An ANSWERED AskUserQuestion carrier is a type:\"user\" record with a NON-errored tool_result block AND a non-empty structured toolUseResult.answers object. Those two signals co-occur on 95/95 corpus carriers, but the THIRD signal (a marker string csift knows) now fails on 16/95 - Claude Code writes a third prefix csift does not list. A fourth carrier shape exists that is neither answer nor error: the idle auto-resolve, non-errored with answers:{} - excluded only because csift requires answers to be NON-EMPTY.",
"depends": "`is_auq_answer_boundary` opens a turn on the structured signal and keeps the marker only as a fallback for older records without `toolUseResult`, so an AUQ answer classifies `user.answer`; keying on the marker alone loses answers on pre-`toolUseResult` records, and keying on the carrier shape alone mints turns out of cancellations.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "248-252",
"snippet": " /// A CANCELLED / rejected / validation-errored AUQ (no `answers`, `is_error:true`,\n /// or a `Cancelled…` / `<tool_use_error>…` body) is NOT a boundary - those carry no\n /// typed user message. Verified on real data: all 81 answered carriers have\n /// non-empty `toolUseResult.answers`, the marker string, and `is_error` false; the\n /// rejection/cancel carriers have none of the three."
},
{
"path": "src/model/predicates.rs",
"lines": "271-279",
"snippet": " if !has_non_errored_tool_result {\n return false;\n }\n // Primary signal: structured, non-empty `toolUseResult.answers`.\n if self.has_auq_answers() {\n return true;\n }\n // Fallback (older records without `toolUseResult`): the synthesized marker.\n self.is_auq_answer()"
}
],
"instrument": "`csift search \"\" @<session> -t user.answer --format json | jq -r .hits[].line`, fetch each with `csift show @<session> --line <n> --raw`, and check every one has `\"answers\":{` with at least one key and no `\"is_error\":true`. Counting rule: AUQ tool_result carriers, split by the three signals.",
"located": {
"claude_code": null,
"csift": null,
"source": "src/model/predicates.rs is_auq_answer_boundary doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<per record: is_error of every tool_result block, emptiness of toolUseResult.answers, and which marker prefix the content starts with>' plus a per-ask outcome census joining every AskUserQuestion tool_use id to its tool_result block inside the same file",
"observed": "95 answered carriers: is_error false on 95/95 tool_result blocks, non-empty toolUseResult.answers on 95/95, but only 79/95 start with one of csift's two known markers ('Your questions have been answered'); the other 16 start with a third phrasing, 'The user answered: '. Per-ask census over 111 distinct AskUserQuestion tool_use ids: 95 answered, 13 errored (is_error true, toolUseResult a plain STRING with no answers map), 3 auto-resolved after idle (is_error absent, toolUseResult {afkTimeoutMs:60000, annotations:{}, answers:{}, questions:[1]}, content starting 'No response after 60s - the user may be away from keyboard').",
"rule": "one row per distinct AskUserQuestion tool_use block id found corpus-wide, joined to the tool_result block carrying the same id in the same transcript",
"note": "Code sites verify verbatim at src/model/predicates.rs:248-252 and 262-279. The primary/fallback ordering is what saves csift here: the 16 third-phrasing records are recognised through toolUseResult.answers, and the marker fallback only runs on records with no toolUseResult (none in this corpus). The doc comment's 'all 81 answered carriers have ... the marker string' is now false and should be reworded."
}
]
},
{
"id": "ELI-004",
"area": "elicitation",
"behavior": "Claude Code 2.1.258 synthesizes the AskUserQuestion tool_result from a five-way branch. 'User has answered your questions' is gone from the binary and never appears as a real marker in this corpus. The two live prefixes are 'Your questions have been answered: ' (every question answered by clicking a listed option, no notes) and 'The user answered: ' (anything else - notes attached, or an answer that is not one of the offered labels); the other three are 'The user responded: ' (freeform text), 'The user did not answer the questions.' and the idle path 'No response after Ns ...' optionally followed by 'Before going idle the user had selected: ...'.",
"depends": "csift recognises an AUQ answer by START-anchored prefix (`trim_start` + `starts_with`) against a LIST of markers, three since v0.10.1: the retired `User has answered your questions` (kept for historical transcripts), `Your questions have been answered`, and 2.1.258's `The user answered:`. A single hardcoded marker missed the dominant form entirely and silently dropped real `user.answer` turns, while a `contains` check false-positived on any tool result that merely QUOTES a marker (which relabeled whole documentation files as `user.answer`). The list is the FALLBACK for records without `toolUseResult` (the primary boundary signal is a non-empty `toolUseResult.answers`); the `The user did not answer the questions.` branch is deliberately excluded because it opens no turn. All three phrasings are also verifiable synth markers in the search prefilter.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "3-9",
"snippet": "/// The synthesized prefixes Claude Code writes into the `tool_result` answering an\n/// `AskUserQuestion` (§4.4). CC has shipped THREE phrasings for the same synthesized\n/// answer - verified across real `~/.claude/projects` data and the 2.1.258 binary:\n///\n/// - `\"User has answered your questions: \\\"<q>\\\"=\\\"<a>\\\". …\"`\n/// - `\"Your questions have been answered: \\\"<q>\\\"=\\\"<a>\\\". …\"` (the dominant form\n/// in older data; a single hardcoded marker missed it entirely)"
},
{
"path": "src/model/markers.rs",
"lines": "15",
"snippet": "pub const AUQ_ANSWER_MARKERS: &[&str] = &["
},
{
"path": "src/model/markers.rs",
"lines": "34-38",
"snippet": "#[must_use]\npub fn is_auq_answer_text(text: &str) -> bool {\n let head = text.trim_start();\n AUQ_ANSWER_MARKERS.iter().any(|m| head.starts_with(m))\n}"
},
{
"path": "src/search/matcher.rs",
"lines": "493",
"snippet": "// The agents-stopped kill notice renders a fabricated `[subagent stopped]` head."
},
{
"path": "src/model/markers.rs",
"lines": "15-22",
"snippet": "pub const AUQ_ANSWER_MARKERS: &[&str] = &[\n \"User has answered your questions\",\n \"Your questions have been answered\",\n // CC 2.1.258's third branch (`The user answered: \"<q>\"=\"<a>\". Read the answers\n // carefully ...`); the fourth branch `The user did not answer the questions.` is\n // the UNANSWERED case and must never open a turn, so it stays out.\n \"The user answered:\",\n];"
},
{
"path": "src/search/matcher.rs",
"lines": "487-495",
"snippet": " let mut verifiable: Vec<&[u8]> = vec![\n b\"<task-notification>\",\n br#\"\"answers\"\"#,\n b\"User has answered your questions\",\n b\"Your questions have been answered\",\n b\"The user answered:\",\n // The agents-stopped kill notice renders a fabricated `[subagent stopped]` head.\n b\"stopped by the user\",\n ];"
}
],
"instrument": "`rg -c 'User has answered your questions' ~/.claude/projects -g '*.jsonl'` versus `rg -c 'Your questions have been answered'`; both must be non-zero over a corpus spanning versions, and a file carrying both spans the transition. Counting rule: one per tool_result content STARTING with the marker, which `csift search \"\" -t user.answer -c` reports directly.",
"located": {
"claude_code": "2.1.258",
"csift": "0.1.0",
"source": "src/model/markers.rs AUQ_ANSWER_MARKERS doc comment; SPEC.md section 4.4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'answered your questions|questions have been answered|The user answered|The user responded|did not answer the questions|Before going idle the user had selected' AND csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<bucket each carrier by the prefix its tool_result content starts with, cross-tabbed with the record version field>'",
"observed": "'User has answered your questions' has ZERO occurrences in the 2.1.258 binary. The binary emits five distinct synthesized results from one branch: `Before going idle the user had selected: ${_}.`, `The user responded: ${r}`, `Your questions have been answered: ${_}. You can now continue with these answers in mind.`, `The user answered: ${_}. Read the answers carefully - they may request clarification, changes, or that you not proceed - and follow what they actually say.`, and `The user did not answer the questions.` The branch chooses 'Your questions have been answered' only when every question was answered by a listed option with no notes. On disk: 79 carriers start with 'Your questions have been answered' (versions 2.1.156 through 2.1.258), 16 start with 'The user answered: ' (2.1.217 through 2.1.251), 0 start with 'User has answered your questions'. The 112 corpus hits for that dead phrase are all quotes inside prose (labels: agent.tool.result 198, agent.tool.use 69, agent.message 24 - zero user.answer).",
"rule": "one count per answered carrier, bucketed by the prefix the tool_result content STARTS with after trim_start; binary counts are literal string occurrences",
"note": "The START-anchored matching design is vindicated (every one of the 112 'User has answered your questions' occurrences in this corpus is a mid-prose quote, and a contains-check would relabel them). Impact today is bounded: is_auq_answer_text is only reached as the fallback in is_auq_answer_boundary (src/model/predicates.rs:279) for records with no toolUseResult, and the search prefilter's `\"answers\"` needle still admits the third-phrasing lines - but the marker list and its doc comment are stale as of 2.1.258. code-site note: src/model/markers.rs:14-17 AUQ_ANSWER_MARKERS lists a phrase 2.1.258 no longer emits and omits the live 'The user answered' / 'The user responded' forms; src/search/matcher.rs:485-492 repeats the same two byte needles in the verifiable synth-marker list."
}
]
},
{
"id": "ELI-005",
"area": "elicitation",
"behavior": "When the operator answers by typing prose instead of clicking, the value in `answers` is the literal placeholder `(notes only)` and their whole message lives in annotations[<verbatim question>].notes. `(no option selected)` is NOT a sibling value in `answers`: it is the string the inline synthesized tool_result substitutes for that same state (2.1.258 renders `\"<q>\"=(no option selected) notes: <prose>`).",
"depends": "csift renders the notes BESIDE the placeholder (`A3: (notes only) note: ...`), it does not replace the placeholder with them - so the placeholder still appears in output, and a reader must not treat its presence as a lost answer.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "37-43",
"snippet": " // `annotations` map (§4.4) - per-question `{notes?, preview?}`; when the answer\n // is the `\"(notes only)\"` placeholder the user's ENTIRE real message lives here.\n let annotations = tur\n .as_ref()\n .and_then(|t| t.get(\"annotations\"))\n .and_then(serde_json::Value::as_object)\n .filter(|m| !m.is_empty());"
},
{
"path": "src/model/exchange.rs",
"lines": "80-88",
"snippet": " // Free-text notes the user attached to THIS answer. When the answer is\n // the `\"(notes only)\"` placeholder, the notes ARE the user's message -\n // dropping them silently swallowed the whole turn (the common path,\n // since the user routinely answers AUQs with typed prose, not a click).\n let note = annotations\n .and_then(|a| a.get(question))\n .and_then(|v| v.get(\"notes\"))\n .and_then(serde_json::Value::as_str)\n .filter(|s| !s.is_empty());"
},
{
"path": "src/model/tests/boundaries.rs",
"lines": "217",
"snippet": "\"answers\":{\"which route for the queue?\":\"(notes only)\"},\"annotations\":{\"which route for the queue?\":{\"notes\":\"never conflate the two — Route A is inbound only, Route B is outbound only\"}}},\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"q1\",\"content\":\"User has answered your questions: \\\"which route for the queue?\\\"=\\\"(notes only)\\\". You can now continue.\"}]}}\"#,"
}
],
"instrument": "`csift search '(notes only)' -t user.answer --no-truncate`: every hit must render the notes prose as the answer, not the placeholder. Counting rule: one unit per answered AUQ carrier. The unit test `auq_exchange_surfaces_notes_when_answer_is_notes_only` in src/model/tests/boundaries.rs pins it on a real-captured shape.",
"located": {
"claude_code": "2.1.150",
"csift": "0.1.0",
"source": "SKILL.md notes-only row; src/model/exchange.rs:37-43 comment; SPEC.md section 4.4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '\\(notes only\\)' -t user.answer -c ; csift show @<session> --line <n> (on the first such hit's own refetch line) ; csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<for every answers value equal to the placeholder, check which placeholder string the inline tool_result text uses>' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'no option selected|notes only'",
"observed": "31 exchanges match the placeholder under -t user.answer. Every one of the 29 placeholder answer values in the corpus is the literal '(notes only)'; in all 29 the inline synthesized string writes '(no option selected)' for the same question, never '(notes only)'. The 2.1.258 binary holds both strings twice each, with the constant `var h6t=\"(notes only)\"` used as the answers value and the renderer `B?`\"${C}\"=\"${I}\"`:`\"${C}\"=(no option selected)`` used for the string. A fetched record renders `A3: (notes only) note: <the operator's prose>` and `A4: (notes only) note: <prose>` - placeholder and notes both present.",
"rule": "one count per (question, answer) pair whose answers value is a placeholder; the render check is one fetched record unit read in full",
"note": "Code sites verify verbatim at src/model/exchange.rs:37-43 and 80-88. In 2.1.258 the same branch also honours annotations[q].preview (`selected preview:\\n<preview>`), which csift does not render."
}
]
},
{
"id": "ELI-006",
"area": "elicitation",
"behavior": "The rendered header is `[AskUserQuestion . N question(s)]` (middle dot, singular for N==1).",
"depends": "csift zips `questions[]` with `answers{}` and renders every pair under one `[AskUserQuestion - N questions]` header, so no intervention is undercounted; counting one intervention per record shrinks every human-turn denominator by roughly 5.5%.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "44-48",
"snippet": " let mut out = String::new();\n let n = questions.map_or(answers.len(), Vec::len);\n out.push_str(&format!(\"[AskUserQuestion · {n} question{}]\", plural(n)));\n if let Some(qs) = questions {\n for (i, q) in qs.iter().enumerate() {"
},
{
"path": "src/model/exchange.rs",
"lines": "89-93",
"snippet": " out.push_str(&format!(\"\\nQ{} \", i + 1));\n if let Some(h) = header {\n out.push_str(&format!(\"({}): \", normalize_line(h)));\n }\n out.push_str(&normalize_line(question));"
}
],
"instrument": "`csift search \"\" @<session> -t user.answer --no-truncate` and read the rendered `[AskUserQuestion - N questions]` header on each unit. Counting rule: one exchange per answered AUQ record; pairs counted from N in that header, so records with N>1 are the multi-question set.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "SKILL.md wrong-assumption table (121 records / 166 pairs)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search \"\" -t user.answer --format json --max-count 0 | python3 -c '<count exchange rows and distinct record uuids>' AND csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<sum len(toolUseResult.questions) and count records with len>1>'",
"observed": "116 exchange rows collapse to 95 distinct record uuids (the 21 duplicates are clone-copied transcripts double-counting the same records). Those 95 carry 118 question-answer pairs; 17 records (17.9%) carry more than one question (13 with 2, 2 with 3, 2 with 4), hiding 23 pairs behind the first question. On the ask side, 106 parseable AskUserQuestion inputs carry 129 questions, 17 of them multi-question (16.0%). Every pair renders under one `[AskUserQuestion . N question(s)]` header with its own `Q<i>` / `A<i>` lines.",
"rule": "one record per distinct uuid csift labels user.answer corpus-wide; pairs = sum of len(toolUseResult.questions) over those records; 'multi' = records with len > 1",
"note": "Code sites verify verbatim at src/model/exchange.rs:44-48 and 89-93. The phenomenon holds and is worth stating; only the absolute numbers were corpus-bound. Any restatement should name the counting rule, since raw hit counts overstate by the clone-duplication factor (116 vs 95 here)."
}
]
},
{
"id": "ELI-007",
"area": "elicitation",
"behavior": "Add the non-errored non-answer shape: an AskUserQuestion that auto-resolves after idle lands with is_error ABSENT, content 'No response after 60s - the user may be away from keyboard...', and toolUseResult {questions, answers:{}, annotations:{}, afkTimeoutMs}. Only the non-EMPTY answers requirement keeps it out of user.answer.",
"depends": "is_auq_answer_boundary refuses any errored carrier before it looks for answers, so a cancelled question never classifies user.answer - but it is NOT true that it never opens a turn: 6 of the 13 errored AUQ carriers here carry the typed 'To tell you how to proceed, the user said:' tail and open a turn through is_plan_rejection_boundary, classified user.rejection (that is correct - the operator did type a message).",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "262-270",
"snippet": " let mut has_non_errored_tool_result = false;\n for b in blocks {\n if let Block::ToolResult { is_error, .. } = b {\n if is_error.unwrap_or(false) {\n return false; // an errored AUQ result (cancel/reject) is never a boundary\n }\n has_non_errored_tool_result = true;\n }\n }"
},
{
"path": "src/model/exchange.rs",
"lines": "20-22",
"snippet": " if !self.is_auq_answer_boundary() {\n return None;\n }"
}
],
"instrument": "`csift search \"\" <target> --count-by result` (buckets `ok` | `error`), then fetch an `error` AUQ carrier with `csift show <target> --line <n>`: it must not be labeled `user.answer`. Counting rule: one record per bucket.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "src/model/predicates.rs is_auq_answer_boundary body; AGENTS.md section 3.3 case 2; SPEC.md section 4.4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "<join every distinct AskUserQuestion tool_use id to the tool_result block carrying it in the same transcript, then bucket by is_error and content shape> followed by csift show @<session> --line <n> on one carrier of each bucket",
"observed": "Of 111 distinct AskUserQuestion asks on disk, 16 have no answer carrier: 6 rejections whose body carries the typed tail 'To tell you how to proceed, the user said:', 2 bare rejections, 5 '<tool_use_error>InputValidationError: AskUserQuestion was called with input that could not be parsed as JSON.', and 3 idle auto-resolves. All 13 rejection/validation carriers have is_error true and a toolUseResult that is a plain STRING ('Error: The user doesn't want to proceed with this tool use. ...'), with no answers map. csift renders a bare/validation one as an agent.tool.use paired with an agent.tool.result marked [error] for AskUserQuestion, and a typed-tail one as user.rejection - never user.answer.",
"rule": "one row per distinct AskUserQuestion tool_use id, bucketed by its tool_result block's is_error flag and content prefix",
"note": "Code sites verify verbatim at src/model/predicates.rs:262-270 and src/model/exchange.rs:20-22. On a rejected AskUserQuestion, Claude Code injects its own prose into the typed tail ('The user wants to clarify these questions. This means they may have additional information, context or questions for you...'), so the user.rejection body on that path is partly harness text rather than purely operator text."
}
]
},
{
"id": "ELI-008",
"area": "elicitation",
"behavior": "A PENDING AskUserQuestion or ExitPlanMode is never flushed to the transcript while it waits: Claude Code buffers the WHOLE assistant turn and commits the `tool_use` to the persisted message array only after the interactive tool resolves (verified across 51 sessions with zero counterexamples; on one measured session the ask's `tool_use` was still absent from disk 37 minutes after its own emit timestamp, and the answering `tool_result` landed about 89 minutes after that timestamp). If such a `tool_use` is on disk, its answer is too - so an option-picker freeze is byte-identical on disk to a stall.",
"depends": "csift cannot render a pending-question state from the native transcript at all; the unanswered window is recoverable only from the hook-written sidecar, whose unresolved pendings `search`/`show`/`verbatim`/`list` merge as if native (a pending marker classifies `agent.tool.use`, renders `(elicitation sidecar)` in place of a line number, and every surface that merged one prints `with elicitation sidecar`). A `status` that inferred 'blocked on a question' from the transcript alone would be fabricating.",
"code": [
{
"path": "src/elicitation.rs",
"lines": "4-7",
"snippet": "//! Three Claude Code elicitations stall a session on a human yet are invisible / ambiguous\n//! in the native jsonl while pending: **AskUserQuestion** and **ExitPlanMode** (CC buffers\n//! the whole assistant turn until answered - nothing on disk during the wait, see §3.4) and\n//! an **MCP Elicitation** (the inner request lives in memory). A Claude Code hook records"
},
{
"path": "src/elicitation.rs",
"lines": "85-89",
"snippet": "/// only to carry what the native jsonl does NOT yet hold. An AUQ/ExitPlanMode turn is\n/// buffered in memory and flushed ONLY when the elicitation closes, so its tool id\n/// appearing on a native record - as a `tool_use` block `id` OR a `tool_result`\n/// `tool_use_id` - proves closure (answered or rejected alike). Such a stale pending is\n/// dropped exactly like a sidecar-resolved pair (the same dedup class, silent by design)."
},
{
"path": "src/model/classify.rs",
"lines": "140-148",
"snippet": " // Elicitation sidecar markers (§3.10): a PENDING marker stands in for the native\n // tool_use CC has not yet written → agent.tool.use (covers the MCP `system` form too,\n // which carries no tool_use block). A RESOLVED marker is a pairing artifact → no label.\n if self.is_elicitation_marker() {\n if self.csift_phase.as_deref() == Some(\"pending\") {\n push_unique(&mut out, Class::AgentToolUse);\n }\n return out;\n }"
}
],
"instrument": "With an AskUserQuestion open on screen, run `csift search AskUserQuestion @<that session> -t agent.tool.use` from another terminal: zero native hits while pending. Re-run after answering and the tool_use appears, timestamped at its EMIT instant rather than the answer instant. Counting rule: one record per tool_use block. Requires a live session - it cannot be observed from a static corpus.",
"located": {
"claude_code": "2.1.150",
"csift": "0.1.0",
"source": "AGENTS.md section 3.4; SPEC.md sections 4.4 and 6.10; src/elicitation.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "unverifiable-here",
"instrument": "ls ~/.claude/sessions/*.json | <read each row's status> AND <join every AskUserQuestion ask id to its tool_result and check for unreturned asks> AND <for each ask/answer pair, compare file order against timestamps: count records physically before the ask whose timestamp falls inside the wait window, and records physically between ask and answer whose timestamp is >5s after the ask>",
"observed": "No live pending dialog exists to observe: the 7 registry rows read idle x5, busy x1 (2.1.258), shell x1 - none 'waiting'. On the static corpus 111 of 111 AskUserQuestion asks that are on disk also have their tool_result on disk (0 unreturned), which is consistent with the claim's corollary but cannot see the pending window. The file-order probe is not decisive and points both ways: 7 ask/answer pairs (2.1.156, 2.1.199 x2, 2.1.220 x4) have a queue-operation line physically BEFORE the ask whose timestamp falls inside the wait window (consistent with the ask being flushed late), while 3 pairs (2.1.199 multi-question, 2.1.228 multi-question, 2.1.228 single-question) have queue-operation lines physically AFTER the ask spread across the wait window (consistent with the ask already being on disk). Both readings depend on an unverified assumption about when a queue-operation line is written.",
"rule": "one row per registry file; one row per distinct AskUserQuestion ask id; for the order probe, one classification per ask/answer pair with a wait longer than 60s",
"note": "What would decide it: with an AskUserQuestion picker open on screen, run `csift search AskUserQuestion @<that session> -t agent.tool.use` (or `csift show @<session> --turn -1 --raw`) from a second terminal and record whether the tool_use is on disk while pending, then re-run after answering. That needs a live session at 2.1.258 and cannot be produced from a static corpus. Code sites verify verbatim at src/elicitation.rs:4-7 and 85-89 and src/model/classify.rs:140-148. Caution for the ledger: the claim's absolute 'never flushed' now sits in tension with ELI-009 in the same ledger, and the file-order evidence above is mixed - the wording should name the versions and shapes it was measured on."
}
]
},
{
"id": "ELI-009",
"area": "elicitation",
"behavior": "Since Claude Code 2.1.258 a MULTI-question AskUserQuestion ask IS written to the transcript at QUESTION time rather than held to the end of the turn, while a single-question ask still stays buffered until answered - so an unreturned `AskUserQuestion` / `ExitPlanMode` `tool_use` at the tail is itself a live blocked-on-a-human signal for the multi-question shape only.",
"depends": "`status`/`wait` carry the tail dialog as its own HITL leg beside the registry `waiting` row and the sidecar, and the evidence line says which leg fired, so a multi-question ask no longer needs the sidecar to be seen and a single-question ask is still honestly attributed to it.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "170-227",
"snippet": " ),\n };\n evidence.push(Evidence {\n surface: \"pid\",\n value: v,\n age_secs: None,\n });\n if let Some(n) = note {\n notes.push(n);\n }\n }\n match &main_tail.unreturned_use {"
},
{
"path": "src/live/verdict.rs",
"lines": "255-262",
"snippet": " String::new()\n }\n ),\n age_secs: None,\n });\n }\n for p in pending_elicitations {\n evidence.push(Evidence {"
}
],
"instrument": "Open a MULTI-question AskUserQuestion in a live session and, while the picker is up, run `csift status @<uuid>` from another terminal: the verdict is `waiting-hitl` with the tail-dialog evidence line, and `csift show @<uuid> --turn -1 --raw` shows the `tool_use` with no paired `tool_result`. Repeat with a SINGLE-question ask: nothing is on disk and only a sidecar pending (or a current registry `waiting` row) can show it. Counting rule: one tool_use id, checked for a matching tool_result id in the same file.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "src/live/verdict.rs:216-220 comment"
},
"first_seen_claude_code": "2.1.258",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "unverifiable-here",
"instrument": "csift search 'AskUserQuestion' -t agent.tool.use --raw --max-count 0 | python3 -c '<group asks by record version and question count>' AND <per ask/answer pair at 2.1.251+, measure line gap, wait seconds and records landing inside the wait window> AND grep -n 'MULTI-question ask is written at question time' src/live/verdict.rs",
"observed": "The corpus holds only two AskUserQuestion pairs at 2.1.258 - one single-question (wait 90s) and one multi-question (wait 244s) - and in BOTH the answer record is on the immediately next line (gap 1) with no record landing inside the wait window, so neither can discriminate question-time from resolve-time writing. Across all versions the earlier per-pair order probe found one 2.1.220 MULTI-question ask that looks flushed LATE and one 2.1.228 multi-question ask that looks written EARLY, so the corpus does not support a clean 'since 2.1.258' boundary either. The claim's cited code site does not hold the claim: src/live/verdict.rs:216-227 and 255-262 are the pid-evidence and sidecar-evidence blocks.",
"rule": "one row per ask/answer pair, keyed by the ask record's version field and its input.questions length",
"note": "What would decide it: open a MULTI-question AskUserQuestion in a live 2.1.258 session and, while the picker is up, run `csift status @<uuid>` and `csift show @<uuid> --turn -1 --raw` from another terminal; repeat with a single-question ask. The registry currently has no 'waiting' row, so no such window exists on this machine right now. Until that is run the claim rests on a code comment, not on an instrument, and the version boundary in particular is unsupported by the corpus. code-site note: The load-bearing site is src/live/verdict.rs:291-295, the comment '// Two HITL legs beside the sidecar: the registry's `waiting` status (the binary sets / // it whenever a dialog blocks the session: a question, a permission prompt, a plan / // approval, a sandbox/worker request), and an unreturned AskUserQuestion/ExitPlanMode / // at the tail (a MULTI-question ask is written at question time since CC 2.1.258; / // a single-question ask stays buffered until answered - the sidecar's shape).', plus the emitted note at src/live/verdict.rs:345-352 ('the tail holds an unreturned AskUserQuestion/ExitPlanMode call: a multi-question ask is written at question time, a single-question ask stays buffered until answered (the sidecar covers that shape)')."
}
]
},
{
"id": "ELI-010",
"area": "elicitation",
"behavior": "An MCP elicitation's inner `elicitation/create` request is an in-memory MCP-client callback: it never becomes a transcript record in any form, so the session gains no record at all between the outer `mcp__*` tool_use and its tool_result while the server waits on the human.",
"depends": "The sidecar's MCP marker is shaped as a `type:\"system\"` record with NO tool_use block; csift's classify gives any pending marker `agent.tool.use` through a guarded arm (and `search` matches the MCP form on its top-level `content`), so `-t agent.tool.use` reaches all three elicitation kinds uniformly.",
"code": [
{
"path": "src/elicitation.rs",
"lines": "4-7",
"snippet": "//! Three Claude Code elicitations stall a session on a human yet are invisible / ambiguous\n//! in the native jsonl while pending: **AskUserQuestion** and **ExitPlanMode** (CC buffers\n//! the whole assistant turn until answered - nothing on disk during the wait, see §3.4) and\n//! an **MCP Elicitation** (the inner request lives in memory). A Claude Code hook records"
},
{
"path": "src/model/classify.rs",
"lines": "140-148",
"snippet": " // Elicitation sidecar markers (§3.10): a PENDING marker stands in for the native\n // tool_use CC has not yet written → agent.tool.use (covers the MCP `system` form too,\n // which carries no tool_use block). A RESOLVED marker is a pairing artifact → no label.\n if self.is_elicitation_marker() {\n if self.csift_phase.as_deref() == Some(\"pending\") {\n push_unique(&mut out, Class::AgentToolUse);\n }\n return out;\n }"
}
],
"instrument": "Trigger an MCP elicitation and confirm the session transcript gains no record between the outer mcp tool_use and its tool_result (`csift show @<session> --turn -1 --raw` before and after answering); `csift search \"\" @<session>` prints the `with elicitation sidecar` note only when the hook wrote the sidecar. Counting rule: one record per jsonl line added during the pending window (expected zero). Requires a live MCP server.",
"located": {
"claude_code": "2.1.193",
"csift": "0.6.0",
"source": "AGENTS.md section 3.10; SPEC.md section 6.10"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "unverifiable-here",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'elicitation/create|notifications/elicitation/complete' AND find ~/.claude/projects -name elicitations.jsonl | python3 -c '<census csiftKind / csiftPhase / type over every sidecar line>' AND csift search 'elicitation/create' -c",
"observed": "2.1.258 registers the inner request as an in-process client handler: `e.setRequestHandler(\"elicitation/create\",(o,i)=>n.handle(o,{signal:i.mcpReq.signal}))`, paired with `e.setNotificationHandler(\"notifications/elicitation/complete\", ...)` whose only side effect in that block is a user notification ('MCP server \"...\" confirmed elicitation ... complete', notificationType 'elicitation_complete'); the surrounding constructor takes runElicitationHooks / runElicitationResultHooks. No MCP elicitation has ever occurred on this machine: the 20 sidecar files hold 260 marker lines whose csiftKind is AskUserQuestion 239 and ExitPlanMode 21 - zero MCP - and the 34 corpus hits for 'elicitation/create' are all prose quotes in development sessions, none an MCP record.",
"rule": "one count per sidecar line grouped by csiftKind; binary evidence is the literal handler registration string",
"note": "The mechanism half is corroborated (an in-process MCP-client request handler, not a transcript writer), but the load-bearing half - that the session gains NO record between the outer mcp__* tool_use and its tool_result - cannot be decided here because no MCP server on this machine has ever issued an elicitation. What would decide it: connect an MCP server that calls elicitation/create, snapshot the transcript line count and `csift show @<session> --turn -1 --raw` during the pending window, and diff after answering. Code sites verify verbatim at src/elicitation.rs:4-7 and src/model/classify.rs:140-148."
}
]
},
{
"id": "ELI-011",
"area": "elicitation",
"behavior": "An MCP elicitation hook DOES receive an elicitation_id at 2.1.258, but the field is declared optional on both the Elicitation and the ElicitationResult schema, while mcp_server_name is required on both. So the server name is the only key guaranteed to be present, which is what makes it unsafe to substring-scan - the claim's conclusion stands but its premise should say 'the only guaranteed key is a non-unique server name', not 'the id may be a server name'. csift's own hook recipe already prefers the unique id: key=$(jq -r '.tool_use_id // .elicitation_id // .mcp_server_name // \"unknown\"').",
"depends": "csift exempts MCP markers from the native-closure cross-check and keeps them sidecar-paired only (the guard runs only for the `AskUserQuestion` / `ExitPlanMode` kinds); substring- scanning a server name against the transcript would falsely close a genuinely pending MCP elicitation.",
"code": [
{
"path": "src/elicitation.rs",
"lines": "91-93",
"snippet": "/// MCP markers are exempt: an MCP elicitation never has a native form, so sidecar pairing\n/// stays its only signal - and its `csiftKey` may be a non-unique server name, unsafe to\n/// substring-scan."
},
{
"path": "src/elicitation.rs",
"lines": "118-125",
"snippet": "/// A pending marker whose kind eventually gets a NATIVE record (keyed by tool_use_id) -\n/// the only kinds the ghost guard may cross-check.\nfn is_native_tool_kind(rec: &Record) -> bool {\n matches!(\n rec.csift_kind.as_deref(),\n Some(\"AskUserQuestion\" | \"ExitPlanMode\")\n )\n}"
},
{
"path": "src/model/record.rs",
"lines": "221-224",
"snippet": " /// The sidecar pairing KEY (tool_use_id / elicitation_id / MCP server) - groups a\n /// `pending` with its later `resolved`. `None` on a native record.\n #[serde(default, rename = \"csiftKey\")]\n pub csift_key: Option<String>,"
}
],
"instrument": "Read the sidecar at `<session sidecar dir>/elicitations.jsonl` and compare each `csiftKind:\"mcp-elicitation\"` key against the native transcript with `csift search '<key>' @<session> --raw`: no record carries it as a tool id. Counting rule: MCP keys found natively over MCP keys total (expected 0).",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.3",
"source": "AGENTS.md section 3.10; SPEC.md section 6.10; src/elicitation.rs drop_natively_closed doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,320}elicitation_id:i\\(\\)\\.optional\\(\\).{0,220}' AND a census of every ~/.claude/projects/*/*/elicitations.jsonl grouped by (csiftKind, csiftKey) AND csift search 'mcp_elicitation|elicitation/create|\"subtype\":\"elicitation\"' --count-by label over the csift project directory under ~/.claude/projects",
"observed": "Both elicitation hook schemas came back verbatim. Elicitation: hook_event_name:x(\"Elicitation\"),mcp_server_name:i(),message:i(),mode:ee([\"form\",\"url\"]).optional(),url:i().optional(),elicitation_id:i().optional(),requested_schema:ge(i(),de()).optional(). ElicitationResult: hook_event_name:x(\"ElicitationResult\"),mcp_server_name:i(),elicitation_id:i().optional(),mode:...,action:ee([\"accept\",\"decline\",\"cancel\"]),content:...optional(). So mcp_server_name is REQUIRED on both events and elicitation_id is OPTIONAL on both. Corpus: 20 sidecar files, 260 lines, 135 (csiftKind,csiftKey) groups; csiftKind counts AskUserQuestion 235, ExitPlanMode 21, absent 4; mcp-elicitation 0. The label census of elicitation-word matches returned 129 records under agent.tool.result 79, agent.tool.use 35, agent.thinking 7, agent.message 5, agent.communication.inbox 2, agent.communication.sent 2, harness.compaction.summary 1 - i.e. only prose about elicitations, no harness elicitation record.",
"rule": "One schema literal per hook event name in the binary. Sidecar: one group per (csiftKind, csiftKey) pair, counted across every elicitations.jsonl under ~/.claude/projects. Native search: one record per matched jsonl record, bucketed by the label csift assigns it.",
"note": "The other half of the claim - that an MCP elicitation has no native transcript form - could not be exercised on this machine. No MCP elicitation has ever fired here: 0 of 260 sidecar lines carry csiftKind mcp-elicitation, and a corpus search over 7579 sessions surfaced no harness-labelled elicitation record, only assistant prose discussing them. What would decide it: register an MCP server that issues elicitation/create, answer one elicitation, then read the owning session jsonl for any record carrying that elicitation. All three cited csift code sites are verbatim at the claimed lines (src/elicitation.rs 91-93 and 118-125, src/model/record.rs 214-217)."
}
]
},
{
"id": "ELI-012",
"area": "elicitation",
"behavior": "The contrast clause needs qualifying. csift's own current code (src/live/verdict.rs 294-295) states that since Claude Code 2.1.258 a MULTI-question AskUserQuestion is written at question time while a single-question ask stays buffered until answered, so 'the whole turn is buffered' is no longer unconditional for AskUserQuestion.",
"depends": "`lifecycle`'s frozen-lane detection and `status`'s in-flight verdict both read an unreturned `tool_use` at the tail as a live block rather than a clean finish; if the assistant commit were deferred like an elicitation, a blocked lane would look like an end-of-turn idle.",
"code": [
{
"path": "src/subagent/lifecycle.rs",
"lines": "35-40",
"snippet": " // TAIL: last record's timestamp == completion (best-effort), whether the transcript\n // terminates with a visible assistant message (a clean finish), AND whether the lane is\n // FROZEN at an unreturned tool_use. The frozen verdict comes from the NEWEST meaningful\n // record only (the first non-metadata record from EOF): if it is an assistant tool_use, no\n // tool_result followed it (it IS the last record) ⇒ the lane is blocked there, NOT done. The\n // terminal_agent_msg walk-back is UNCHANGED for every non-frozen lane."
}
],
"instrument": "Run Claude Code without bypassed permissions, trigger a Bash command that prompts, and while the approval dialog is up run `csift show @main --turn -1`: the tool_use record must be present with no paired tool_result. Counting rule: one tool_use id, checked for a matching tool_result id in the same file.",
"located": {
"claude_code": "2.1.193",
"csift": "0.6.0",
"source": "AGENTS.md section 3.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift status @trap:SilverPineHollow5821 (a self-probe run from inside the very Bash call it describes) AND a scan of all 64 top-level transcripts under ~/.claude/projects for a transcript whose last user/assistant record is an assistant tool_use with no tool_result for that id anywhere in the file AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,150}hook_event_name:\"PermissionRequest\".{0,260}'",
"observed": "The self-probe printed: verdict running / registry status busy / pid alive (start-time guard matched) / 'tail unreturned Bash call (1s ago)'. The tool_use for the still-executing Bash call was already on disk while its tool_result did not exist. Corpus scan: 1 of 64 top-level transcripts ends at an unreturned tool_use; its tool is Bash, its record carries stop_reason tool_use, its record timestamp is 2026-07-22T06:25:33Z and the file has not been written for ~42 days - a permanently frozen lane whose tool_use was persisted and whose result never was. Binary: async function*Zte(...){t(`executePermissionRequestHooks called for tool: ${e}`);let C={...Sa(o.session,ne(),d,o),hook_event_name:\"PermissionRequest\",tool_name:e,tool_input:r,permission_suggestions:f};yield*Qy({session:o.session,hookInput:C,toolUseID:n,...})} - the permission prompt is dispatched with the tool_use id already in hand.",
"rule": "One transcript per top-level session file. 'Frozen tail' = the last record of type user or assistant is an assistant record containing a tool_use block whose id appears in no tool_result block anywhere in the same file. One hook-input literal per event name in the binary.",
"note": "No live approval dialog was held open during this verification, so the ordering was observed for a running tool and for a permanently frozen one, not for a tool sitting behind an approval prompt. The permission-specific case rests on the PermissionRequest hook being dispatched with an existing toolUseID. What would decide it outright: trigger a permission prompt in one session, leave it up, and from a second session run csift show on that session's last turn - the tool_use must be present with no paired tool_result. The multi-question AskUserQuestion split named in the correction was also not instrumented here; 0 of 64 top-level transcripts ended at an unreturned AskUserQuestion, so the corpus neither confirms nor refutes it. What would decide that: open a two-question ask, leave it unanswered, and read the transcript tail from another session. The cited code site src/subagent/lifecycle.rs 35-40 is verbatim at the claimed lines."
}
]
},
{
"id": "ELI-013",
"area": "elicitation",
"behavior": "The registry status closed set is exactly busy | shell | idle | waiting, as claimed. But 'without distinguishing which' is wrong: a waiting row carries a companion waitingFor reason, computed by o_o and persisted only while status is waiting. Six values were read out of the binary - input needed (a queued elicitation), dialog open, sandbox request, worker request, goal proposal, and the dialog-kind default permission prompt. So a pending permission prompt IS distinguishable from a question or a plan approval in the registry row, even though it still leaves no transcript trace.",
"depends": "csift never claims to see one: `status` attaches an unconditional honesty note to `idle-eot`, `waiting-children` and `idle-background-open` saying a pending permission prompt would masquerade as idle, and names the registry row as the only witness - transition- written, never a heartbeat, so a stale row from a dead session still needs the pid probe to be refuted.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "305-308",
"snippet": " // never a running shape. `busy` is the only registry running signal.\n let registry_status = registry.and_then(|r| r.status.as_deref());\n let registry_shell = registry_status == Some(\"shell\");\n let running_shape = main_tail.unreturned_use.is_some() || registry_status == Some(\"busy\");"
}
],
"instrument": "Trigger a permission prompt in one session and leave it unanswered, then from another session run `csift status @<uuid>` and `csift search 'permission' @<uuid>`: the transcript carries no marker for the prompt, and the verdict reaches `waiting-hitl` only via a current registry row reading `waiting`. Counting rule: one blocked session, one status read; the absence in the transcript is the observation.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "AGENTS.md section 3.9; src/live/verdict.rs F7 honesty note"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\\[\"busy\",\"shell\",\"idle\",\"waiting\"\\].{0,200}' AND rg -o 'function o_o\\(T\\)\\{.{0,700}' AND rg -o '.{0,140}waitingFor\\?\\?\"permission prompt\".{0,140}' AND a read of every ~/.claude/sessions/<pid>.json AND a scan of the csift project directory under ~/.claude/projects for every rejected tool_use / rejection tool_result pair, listing the record types between them",
"observed": "Status validator, verbatim: var je=[\"busy\",\"shell\",\"idle\",\"waiting\"];function Ke(e){return je.includes(e)?e:void 0} - and the row parser uses it as status:Ke(o.status). Status computer, verbatim: function XBe(T){let I=o_o(T);if(I!==void 0)return{status:\"waiting\",waitingFor:I,working:!1};return{status:T.isLoading||T.delegatedActive?\"busy\":\"idle\",waitingFor:void 0,working:T.isQueryActive}}. Reason computer, verbatim: function o_o(T){if(T.queuedElicitation)return\"input needed\";if(T.topDialogWaitingFor!==void 0)return T.topDialogWaitingFor;if(T.pendingWorkerRequest)return\"worker request\";if(T.pendingSandboxRequest)return\"sandbox request\";if(T.isShowingLocalJSXCommand&&!T.isResponseStreaming&&!T.delegatedActive)return\"dialog open\";return}. Dialog-kind default, verbatim: function fvt(T){return T===void 0?void 0:zS[T]?.waitingFor??\"permission prompt\"}. Shell relabel, verbatim: Nne=VA===\"idle\"&&PIr?\"shell\":VA. Serializer gate, verbatim: ...t.status===\"waiting\"&&t.waitingFor&&{waitingFor:t.waitingFor}. Live registry: 7 rows, statuses idle 5, busy 1, shell 1, none waiting, and every row carried a status key while none carried waitingFor. Transcript trace: 6 rejected tool_use/tool_result pairs; record types appearing between the tool_use and its rejection result were attachment (4 pairs), nothing at all (1 pair), and two queue-operation lines (1 pair) - zero user, assistant or system records; the gaps ran 1.3s to 1270.6s.",
"rule": "One registry row per ~/.claude/sessions/<pid>.json file. One rejection pair per tool_use id whose tool_result is is_error true and whose text begins 'The user doesn't want to proceed'. 'Between' = every line strictly between the two records in file order, counted by its top-level type field.",
"note": "waitingFor is not new in 2.1.258 - the same status:\"waiting\",waitingFor construction is present in the 2.1.229 binary, which predates the version this claim was located at, so this is a gap in the claim's wording rather than a change in Claude Code. The field itself was not observed on disk because no session was in the waiting state during the read; the serializer gates it on status===\"waiting\". What would decide that: open any blocking dialog and read that session's ~/.claude/sessions/<pid>.json while it is up. The cited src/live/registry.rs 5-11 is verbatim at the claimed lines; the src/live/verdict.rs snippet has moved from 292-310 to 305-323 (the block now opens with two new lines about multi-question asks)."
}
]
},
{
"id": "ELI-014",
"area": "elicitation",
"behavior": "Two wording corrections. (1) session_id and transcript_path are not cached at bootstrap - they are recomputed on every hook dispatch from the session object (session_id:e.id, transcript_path:lm(e.id)); the net effect is the claimed one, that they always name the top-level session. (2) Claude Code 2.1.258 adds a sibling field agent_transcript_path that DOES name the subagent's own transcript; it is added alongside agent_id on the SubagentStop input (agent_id:d,agent_transcript_path:Ud(d)), never on the elicitation hooks, and transcript_path itself still names the top-level session.",
"depends": "The elicitation sidecar path is derived from the hook's `session_id`, so it always lands in the TOP-LEVEL session's sidecar dir (the `<uuid>/` dir beside `subagents/`); csift merges it only when reading a top-level transcript, a subagent lane skips the read entirely, and a subagent's MCP elicitation surfaces under its parent - agent-blind by construction, which csift does not pretend otherwise.",
"code": [
{
"path": "src/elicitation.rs",
"lines": "31-35",
"snippet": "//! ## Keyed by the TOP-LEVEL session\n//!\n//! The sidecar always lives beside the TOP-LEVEL session jsonl (the hook's `session_id` is\n//! the top-level/leader uuid, never a subagent's). Callers therefore merge the sidecar only\n//! when reading a top-level session file - a subagent transcript has none."
},
{
"path": "src/elicitation.rs",
"lines": "47-53",
"snippet": "/// The `elicitations.jsonl` path for a session jsonl, or `None` when the session has no\n/// sidecar dir / the path has no stem. The sidecar dir is `<ENC>/<uuid>/` (the same dir that\n/// holds `subagents/`); the marker file sits inside it.\n#[must_use]\npub fn sidecar_path(session_jsonl: &Path) -> Option<PathBuf> {\n Some(crate::subagent::sidecar_dir_for_session(session_jsonl)?.join(SIDECAR_FILE))\n}"
}
],
"instrument": "Install a trivial PreToolUse hook that appends its stdin JSON to a file, run a tool from inside a subagent, and compare the recorded `session_id` with the subagent transcript's own basename: they must differ, `session_id` must equal the top-level uuid, and `agent_id` must name the subagent. Counting rule: one hook invocation per tool call.",
"located": {
"claude_code": "2.1.193",
"csift": "0.2.0",
"source": "AGENTS.md section 3.10; SPEC.md section 6.10; src/elicitation.rs:31-35 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Sa\\(e,n,r,o\\)\\{.{0,900}' AND rg -o '.{0,180}hook_event_name:\"Elicitation\".{0,300}' AND rg -o '.{0,200}hook_event_name:\"PreToolUse\".{0,320}' AND rg -o 'agent_id:i\\(\\)\\.optional\\(\\)\\.describe\\(\"[^\"]{0,400}' AND a check that every elicitations.jsonl under ~/.claude/projects sits beside a top-level transcript, plus a comparison of each sidecar line's recorded sessionId against its owning directory name",
"observed": "Base builder, verbatim: function Sa(e,n,r,o){...return{session_id:e.id,transcript_path:lm(e.id),cwd:n,scratchpad_dir:...,prompt_id:...,permission_mode:r,agent_id:o?.agentId,agent_type:d,effort:v}} - session_id and transcript_path both come from the session object, agent_id from the fourth options argument. Transcript path builder, verbatim: function lm(e){if(e===Q())return cl()??El();let n=fl(ye());return Uf(n,`${e}.jsonl`)} - a flat <session-id>.jsonl, never a subagents/ path. Call sites: PreToolUse uses Sa(o.session,ne(),d,o), PostToolUse uses Sa(d.session,ne(),f,d), PostToolUseFailure uses Sa(d.session,ne(),_,d) - four arguments, so agent_id is populated; Elicitation uses Sa(e,ne(),d) and ElicitationResult uses Sa(e,ne(),d) - THREE arguments, so agent_id is undefined on both. Schema describe, verbatim: 'Subagent identifier. Present only when the hook fires from within a subagent (e.g., a tool called by an AgentTool worker). Absent for the main thread, even in --agent sessions. Use this field (not agent_type) to distinguish subagent calls from main-thread calls.' On-disk: all 20 elicitations.jsonl files sit beside a top-level <uuid>.jsonl, 0 under any subagents/ path, and 260 of 260 sidecar lines carry a sessionId equal to the owning top-level directory name.",
"rule": "One hook-input construction per event name in the binary; argument count of the shared base builder decides whether agent_id is populated. On disk: one sidecar file per session directory; one comparison per sidecar line that carries a sessionId.",
"note": "The on-disk half only exercises main-thread firings, because AskUserQuestion and ExitPlanMode cannot fire in a subagent lane at all (see ELI-017), so no subagent-context elicitation exists in this corpus. The subagent case rests on the binary: the base builder reads the session object for session_id/transcript_path and takes agent_id from a separate options argument, and the schema text says agent_id, not session_id, is the field that distinguishes a subagent call. Both cited code sites (src/elicitation.rs 31-35 and 47-53) are verbatim at the claimed lines."
}
]
},
{
"id": "ELI-015",
"area": "elicitation",
"behavior": "Claude Code exposes distinct hook events `Elicitation` and `ElicitationResult` for an MCP elicitation's open and close, alongside `PreToolUse`, `PostToolUse` and `PostToolUseFailure` for the tool-shaped elicitations - five events in total cover the three blocking kinds.",
"depends": "csift's sidecar recipe subscribes all five with one script: `PreToolUse` writes the `pending` line, `PostToolUse`/`PostToolUseFailure` and `ElicitationResult` write the `resolved` close marker, so every kind is paired by `csiftKey` without touching the native transcript.",
"code": [
{
"path": "SKILL.md",
"lines": "568-573",
"snippet": "case \"$ev\" in\n PreToolUse) case \"$tool\" in AskUserQuestion|ExitPlanMode) kind=\"$tool\"; phase=\"pending\";; esac ;;\n PostToolUse|PostToolUseFailure) case \"$tool\" in AskUserQuestion|ExitPlanMode) kind=\"$tool\"; phase=\"resolved\";; esac ;;\n Elicitation) kind=\"mcp-elicitation\"; phase=\"pending\" ;;\n ElicitationResult) kind=\"mcp-elicitation\"; phase=\"resolved\" ;;\nesac"
}
],
"instrument": "Register a hook under an invalid event name and read the harness error, which enumerates the recognized events; or trigger an MCP elicitation with the recipe installed and confirm the sidecar gains one `csiftPhase:\"pending\"` line on open and one `csiftPhase:\"resolved\"` line on close. Counting rule: one sidecar line per event fired.",
"located": {
"claude_code": "2.1.193",
"csift": "0.6.0",
"source": "AGENTS.md section 3.10; SKILL.md elicitation-marker hook recipe"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -n '^(PreToolUse|PostToolUse|PostToolUseFailure|Elicitation|ElicitationResult)$' AND rg -o '\\[\"PreToolUse\",\"PostToolUse\",\"PostToolUseFailure\",\"PostToolBatch\",\"Notification\"[A-Za-z\",]{0,2000}'",
"observed": "All five bare event-name strings are present, one occurrence each. The canonical event array came back as a single literal with 33 entries: [\"PreToolUse\",\"PostToolUse\",\"PostToolUseFailure\",\"PostToolBatch\",\"Notification\",\"UserPromptSubmit\",\"UserPromptExpansion\",\"SessionStart\",\"SessionEnd\",\"Stop\",\"StopFailure\",\"SubagentStart\",\"SubagentStop\",\"PreCompact\",\"PostCompact\",\"PreModelSwitch\",\"PostModelSwitch\",\"PermissionRequest\",\"PermissionDenied\",\"Setup\",\"TeammateIdle\",\"TaskCreated\",\"TaskCompleted\",\"Elicitation\",\"ElicitationResult\",\"ConfigChange\",\"WorktreeCreate\",\"WorktreeRemove\",\"InstructionsLoaded\",\"CwdChanged\",\"FileChanged\",\"DirectoryAdded\",\"MessageDisplay\"] - all five named by the claim appear in it. The two elicitation events also carry their own schema descriptions: 'Hook input for the Elicitation event. Fired when an MCP server requests user input. Hooks can auto-respond (accept/decline) instead of showing the dialog.' and 'Hook input for the ElicitationResult event. Fired after the user responds to an MCP elicitation. Hooks can observe or override the response before it is sent to the server.'",
"rule": "One event name per entry of the canonical hook-event array literal; membership checked by exact string equality against the five names the claim asserts.",
"note": "All five events exist and the two elicitation events are documented as the open and close of an MCP elicitation, exactly as claimed. Worth recording alongside: the array holds 33 events in total, among them PermissionRequest and PermissionDenied, which the csift sidecar recipe does not subscribe - relevant to ELI-013, since a hook could record permission prompts the same way. The cited SKILL.md 562-567 recipe fragment is verbatim at the claimed lines."
}
]
},
{
"id": "ELI-016",
"area": "elicitation",
"behavior": "The measurement should now read: 12 of 12 real unpaired sidecar keys across 7 sessions were rejections (9 AskUserQuestion, 3 ExitPlanMode), not 6 of 6 across 4 sessions. Every one of the 12 was already on its native transcript as a tool_use id with a rejection tool_result, and csift reported none of them as pending.",
"depends": "csift's ghost-pending guard drops a sidecar-unresolved pending whose `csiftKey` appears STRUCTURALLY on a native record (as a `tool_use` block `id` or a `tool_result` `tool_use_id`; a key merely quoted in prose fails the block-id check), because the native transcript outranks the sidecar. Without it a rejected elicitation is reported pending forever by `list`, `search` and `status`, duplicated beside its own flushed native record. Cost is paid only when at least one AUQ/ExitPlanMode key is sidecar-unresolved: one mmap plus a per-key byte scan, parsing only the lines that contain the key bytes.",
"code": [
{
"path": "src/elicitation.rs",
"lines": "79-84",
"snippet": "/// Claude Code fires NO `PostToolUse` hook for a REJECTED AskUserQuestion / ExitPlanMode\n/// (a rejection is not a tool completion), so the sidecar's `resolved` marker is never\n/// written on that path - sidecar-internal pairing alone would then report the elicitation\n/// as pending FOREVER, while the native transcript long since holds the flushed `tool_use`\n/// plus its rejection `tool_result` (verified on real data: every observed ghost was a\n/// rejection). The native record is the higher-authority truth source - the sidecar exists"
},
{
"path": "src/elicitation.rs",
"lines": "95-98",
"snippet": "/// Cost: paid ONLY when ≥1 AUQ/ExitPlanMode key is sidecar-unresolved (rare - typically\n/// zero). One mmap + a per-key `memmem` byte scan; only the few lines containing the key\n/// bytes are parsed, and a key merely QUOTED in prose (e.g. a Bash command grepping for it)\n/// fails the structural block-id check and does not count as closure."
},
{
"path": "src/elicitation.rs",
"lines": "127-131",
"snippet": "/// True when the native transcript bytes hold a record whose `tool_use` block `id` or\n/// `tool_result` `tool_use_id` equals `key` - STRUCTURAL, not substring: each `memmem` hit\n/// expands to its enclosing line and only that line is parsed, so a key quoted inside some\n/// other record's text never counts as closure.\nfn native_closes(bytes: &[u8], key: &str) -> bool {"
}
],
"instrument": "With the elicitation hook installed, open a plan and REJECT it: `<uuid>/elicitations.jsonl` holds a `csiftPhase:\"pending\"` line with no matching `resolved` line, while the native transcript already carries that same key as a `tool_use` id (`csift show @<session> --uuid <the tool_use id>`), and `csift list @<session>` reports no pending elicitation. Counting rule: group sidecar lines by `csiftKey`, one group per elicitation. The unit test `native_closes_on_a_structural_tool_use_id` in src/elicitation.rs pins the structural check.",
"located": {
"claude_code": "2.1.150",
"csift": "0.6.3",
"source": "AGENTS.md section 3.10; SPEC.md section 4.4 and the v0.6.3 ledger; CHANGELOG 0.6.3; src/elicitation.rs drop_natively_closed doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,1100}for await\\(let ys of \\$Pe\\(o,e,n,_,Ie,nr,Cn\\|\\|Rn.{0,60}' AND a pairing of every sidecar group with no resolved line against its own session transcript, checking whether the csiftKey appears as a structural tool_use block id and what its tool_result says AND csift list @<session> --no-subagents for each such session, grepped for a pending/elicitation annotation",
"observed": "Binary: the tool-execution catch block fires the PostToolUseFailure runner and returns - let nr=$Lo(xn,o.abortController.signal),...,Rr=[];for await(let ys of $Pe(o,e,n,_,Ie,nr,Cn||Rn,v,C,I,An))Rr.push(ys);if(Rn)return... - while the PostToolUse runner (v9) is only reached on the success path after a tool result exists, so a rejection reaches PostToolUseFailure and never PostToolUse. Corpus: 20 sidecar files, 135 groups, 13 groups with no resolved line, of which 1 is a schema-skew group with no csiftKind (4 lines) and 12 are real. All 12 real unpaired keys - 9 AskUserQuestion and 3 ExitPlanMode, spread over 7 sessions - were found on their native transcript as a structural tool_use block id, and all 12 tool_results were is_error true beginning 'The user doesn't want to proceed with this tool use. The tool use was rejected'. 12 of 12 were rejections. csift list on all 7 of those sessions printed no pending or elicitation annotation, so the ghost guard dropped every one.",
"rule": "Group sidecar lines by (csiftKind, csiftKey); one group per elicitation. A group is unpaired when no line in it has csiftPhase resolved. A key is natively closed when some record in the owning transcript carries it as a tool_use block id or a tool_result tool_use_id. A rejection is a tool_result with is_error true whose text starts 'The user doesn't want to proceed'.",
"note": "The census also turned up one 13th unpaired group that is not a rejection and not a ghost: 4 lines carrying the csift sentinel but no readable csiftKind or csiftPhase - the pre-release field-name fossil that csift counts and refuses to merge. Worth keeping in the claim's arithmetic so a future re-count of '13 unpaired groups' is not read as 13 ghosts. All three cited code sites (src/elicitation.rs 79-84, 95-98, 127-131) are verbatim at the claimed lines, and the unit test native_closes_on_a_structural_tool_use_id exists at src/elicitation.rs:508."
}
]
},
{
"id": "ELI-017",
"area": "elicitation",
"behavior": "Three corrections. (1) ExitPlanMode is NOT unconditionally main-thread-only: the agent pool filter re-admits it whenever the agent's own permissionMode is plan (if(an(C,kc)&&f===\"plan\")return!0) and force-appends it if the pool lacks it, so only AskUserQuestion is excluded from every agent context without exception. (2) The measured absence is now 0 occurrences of either tool across 7515 subagent transcripts, not 5806. (3) The MCP figure is 8 subagent transcripts carrying an mcp__ tool_use, not 193 - and the mechanism is stronger than 'allowed': the mcp__ test is the filter's first branch and returns true before any exclusion is consulted.",
"depends": "Keying the elicitation sidecar on the top-level session alone is sound only because the two invisible tool-shaped blockers cannot occur in a subagent lane; a subagent's MCP elicitation is the one kind that can, and it surfaces under the parent session that owns the sidecar.",
"code": [
{
"path": "src/elicitation.rs",
"lines": "31-35",
"snippet": "//! ## Keyed by the TOP-LEVEL session\n//!\n//! The sidecar always lives beside the TOP-LEVEL session jsonl (the hook's `session_id` is\n//! the top-level/leader uuid, never a subagent's). Callers therefore merge the sidecar only\n//! when reading a top-level session file - a subagent transcript has none."
},
{
"path": "SPEC.md",
"lines": "1851",
"snippet": "AskUserQuestion/ExitPlanMode are main-thread-only, and a subagent's MCP elicitation surfaces under its top-level session (agent-blind — the Elicitation hook carries no `agent_id`)"
}
],
"instrument": "`csift search 'AskUserQuestion|ExitPlanMode' . -t agent.tool.use --count-by session`, then compare the keys against `csift list . --format json | jq -r 'select(.is_subagent==true).session_id'`: no subagent id may appear. Counting rule: one record per matched tool_use record.",
"located": {
"claude_code": "2.1.193",
"csift": "0.6.0",
"source": "AGENTS.md section 3.10; SPEC.md section 6.10"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function dAo\\(.{0,900}' AND rg -o '.{0,120}XNe=fe\\(\"external\"\\),lZn=new Set\\(\\[\\.\\.\\.XNe\\]\\).{0,300}' AND rg -o 'function zh\\(.{0,160}' AND csift search 'AskUserQuestion|ExitPlanMode' -t agent.tool.use --format json --max-count 0 (no target, so all projects) filtered to hits whose tool_name is one of the two AND csift search 'mcp__' -t agent.tool.use --format json --max-count 0 filtered to hits whose tool_name starts with mcp__ AND find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl | wc -l, cross-checked with rg -l on the same file list",
"observed": "Exclusion set, verbatim: function fe(e){return new Set([hP,kc,_A,...Vtr,Ki,iG,KNe,uJ,nG,rG,...e!==\"ant\"?[Yc]:[],wa,toe,fbt,_b])}var XNe=fe(\"external\"),lZn=new Set([...XNe]); with var r_=\"ExitPlanMode\",kc=\"ExitPlanMode\" and Ki=\"AskUserQuestion\" - both tool names are members. Agent pool filter, verbatim: function dAo({tools:e,isBuiltIn:n,agentType:r,isAsync:o=!1,isTeammate:d=!1,permissionMode:f,agentDepth:_=0}){let v=e.filter((C)=>{if(zh(C))return!0;if(an(C,kc)&&f===\"plan\")return!0;if(ax(C,XNe))return!1;...});if(f===\"plan\"&&!v.some((C)=>an(C,kc)))v.push(_6);return v}. MCP predicate, verbatim: function zh(e){return e.name?.startsWith(\"mcp__\")||e.isMcp===!0} - the first test in the filter, returning true unconditionally. Corpus (scope line: 7579 sessions, 64 top-level, 7515 subagent): 134 AskUserQuestion and 16 ExitPlanMode tool_use records, every one with is_subagent false; 0 in any subagent transcript. On disk there are 7515 subagent transcripts (160 journal.jsonl files excluded). mcp__ tool_use records: 55 across 4 top-level transcripts and 12 across 8 subagent transcripts; an independent rg -l '\"name\":\"mcp__' over the same 7515 files also returned 8.",
"rule": "One record per matched jsonl record; a record counts for a tool only when csift's structural tool_name field equals it, not when the name merely appears in the rendered text. Subagent transcript = a .jsonl under a subagents/ path directory, excluding journal.jsonl. 'Transcripts carrying an mcp__ tool_use' = distinct session ids among hits whose tool_name starts with mcp__.",
"note": "The plan-mode exception is not new. The 2.1.229 binary carries the same branch - gS({tools:e,isBuiltIn:t,isAsync:r=!1,isTeammate:n=!1,permissionMode:o,agentDepth:i=0}){let s=e.filter((a)=>{if(IL(a))return!0;if($a(a,$2)&&o===\"plan\")return!0;...}) - which predates the version this claim was located at, so the gap is in the claim's wording, not in Claude Code. The 193 figure could not be reproduced under any counting rule I could construct: 8 subagent transcripts contain an mcp__ tool_use, while 7482 of 7515 contain the bytes mcp__ somewhere (the injected tool list), so neither rule yields 193. What would decide the ExitPlanMode exception in practice: run a subagent in plan permission mode and check whether it can emit an ExitPlanMode tool_use. The SPEC.md sentence cited by the claim has moved from line 1851 to line 1861."
}
]
},
{
"id": "ELI-018",
"area": "elicitation",
"behavior": "A session's sidecar directory (the `<uuid>/` dir beside the transcript, the same dir that holds `subagents/`) is created LAZILY: it does not exist at session start and appears only when something first writes into it, so an elicitation sidecar file is typically born mid-session, well after any watcher started.",
"depends": "`csift wait` re-attempts sidecar discovery on EVERY poll instead of only at start, and seeds a newly discovered file at baseline offset 0 because a file born after start is wholly post-start; the startup-only lookup meant a sidecar born mid-wait was never watched, so `--until auq` timed out while the final assessment said waiting-hitl.",
"code": [
{
"path": "src/subagent/discover.rs",
"lines": "5-14",
"snippet": "/// The sidecar directory `<ENCODED>/<session-uuid>/` for a top-level session jsonl,\n/// or `None` if the session has no sidecar. The sidecar is named after the session\n/// uuid (the jsonl basename without `.jsonl`).\n#[must_use]\npub fn sidecar_dir_for_session(session_jsonl: &Path) -> Option<PathBuf> {\n let stem = session_jsonl.file_stem()?.to_str()?;\n let parent = session_jsonl.parent()?;\n let dir = parent.join(stem);\n dir.is_dir().then_some(dir)\n}"
},
{
"path": "src/live/wait.rs",
"lines": "113-129",
"snippet": " // ── Sidecar discovery: the sidecar DIR is typically born mid-wait (the first\n // pending ask creates it), and its path resolves only once the dir exists -\n // so re-attempt each poll. A file born after start is wholly post-start, so\n // baseline 0 is exact. ──\n if !is_subagent_target {\n if let Some(sc) = crate::elicitation::sidecar_path(&main) {\n if !cursors.iter().any(|c| c.path == sc) {\n let lane = crate::subagent::session_id_from_path(&main);\n cursors.push(Cursor {\n path: sc,\n offset: 0,\n is_main: true,\n lane,\n });\n }\n }\n }"
}
],
"instrument": "Run `csift wait @<uuid> --until auq --timeout 60 --interval 50` against a session that has no sidecar directory yet, then trigger the session's first AskUserQuestion: the condition must fire rather than time out. Counting rule: one discovery attempt per poll, one wait per ask. The e2e test `p10_wait_sees_a_sidecar_ask_that_lands_mid_wait` in tests/cli/live/wait.rs pins the mid-wait discovery.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "CHANGELOG 0.9.0; src/live/wait.rs:110-113 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "For every top-level transcript under ~/.claude/projects, compare stat -f %B on its sibling <uuid>/ directory and on that directory's elicitations.jsonl against the transcript's FIRST record timestamp (parsed from the jsonl, not from file metadata)",
"observed": "64 top-level transcripts; 42 have a <uuid>/ sidecar directory; 20 have an elicitations.jsonl. Directory birth minus first-record timestamp: n=42, min +1s, median +3262s, max +1101283s - 42 of 42 strictly after the session's first record. Sidecar-file birth minus first-record timestamp: n=20, min +24s, median +98230s, max +1258058s - 20 of 20 strictly after. Not one directory and not one sidecar file existed at session start.",
"rule": "One measurement per top-level transcript that has a sibling directory named after its uuid. Session start = the timestamp of the first record in the transcript that carries one. Birth time = the APFS creation time from stat -f %B. 'Lazy' = birth strictly greater than session start.",
"note": "File birth times alone are not a safe instrument here - measured against the transcript FILE's birth time the same directories appear to predate it (median -148026s), because forked and copied transcripts carry record timestamps predating their own file. Anchoring on the first record's timestamp is what makes the rule sound, and under that rule the result is unanimous: 42/42 directories and 20/20 sidecar files were born after their session started, the median sidecar arriving more than a day in. That is exactly the condition the wait loop's per-poll rediscovery exists for. Both cited code sites are verbatim at the claimed lines (src/subagent/discover.rs 5-14, src/live/wait.rs 110-126), and the e2e test p10_wait_sees_a_sidecar_ask_that_lands_mid_wait exists at tests/cli/live/wait.rs:175."
}
]
},
{
"id": "ELI-019",
"area": "elicitation",
"behavior": "Claude Code hands a hook its payload as ONE JSON object on the hook process's STDIN. The keys a tool-shaped hook reads are `hook_event_name`, `tool_name`, `tool_input`, `session_id` and `transcript_path`; a tool elicitation additionally carries `tool_use_id`, and the two MCP elicitation events carry `elicitation_id` and `mcp_server_name` instead.",
"depends": "csift's elicitation-sidecar hook recipe parses exactly those keys: `hook_event_name` + `tool_name` pick the kind and phase, `tool_use_id` / `elicitation_id` / `mcp_server_name` build the `csiftKey` the merge pairs `pending` against `resolved` on, and `transcript_path` derives the sidecar path (with a `session_id` glob as fallback). A renamed key never errors - the script exits 0, the sidecar is never written, and `list`'s `sidecar_present` reads false, which is the honest \"hook unknown, cannot conclude\" arm rather than a wrong \"nothing pending\".",
"code": [
{
"path": "SKILL.md",
"lines": "566",
"snippet": "ev=$(jq -r '.hook_event_name//empty' <<<\"$in\" 2>/dev/null); tool=$(jq -r '.tool_name//empty' <<<\"$in\" 2>/dev/null)"
},
{
"path": "SKILL.md",
"lines": "575-576",
"snippet": "tp=$(jq -r '.transcript_path//empty' <<<\"$in\" 2>/dev/null); sid=$(jq -r '.session_id//empty' <<<\"$in\" 2>/dev/null)\nkey=$(jq -r '.tool_use_id // .elicitation_id // .mcp_server_name // \"unknown\"' <<<\"$in\" 2>/dev/null)"
},
{
"path": "src/model/record.rs",
"lines": "210-214",
"snippet": " /// The sidecar record's pairing PHASE - `\"pending\"` (an unanswered elicitation,\n /// missing from the native transcript) or `\"resolved\"` (a lightweight close marker\n /// used only for pairing). Read by [`crate::elicitation`]. `None` on a native record.\n #[serde(default, rename = \"csiftPhase\")]\n pub csift_phase: Option<String>,"
},
{
"path": "src/session/rows.rs",
"lines": "68-73",
"snippet": " /// True when the session's elicitation SIDECAR FILE exists at all (= the csift hook\n /// is installed for this session - resolved pairs stay in the file). The tri-state a\n /// consumer needs: present+pending / present+none (safe to conclude \"not blocked on\n /// an elicitation\") / absent (hook unknown - CANNOT conclude anything). Always false\n /// for a subagent row (the sidecar is keyed by the top-level session).\n pub sidecar_present: bool,"
}
],
"instrument": "Do NOT tally the QUOTED key (`\"hook_event_name\"`): the harness builds its hook payload as a JS object literal with bare keys, so the quoted form is 0 for hook_event_name and elicitation_id. Tally the bare token (`grep -oF hook_event_name`) or, better, read the four payload constructors directly with `strings -n 40 \"$bin\" | grep -oE 'hook_event_name:\"(PreToolUse|PostToolUse|Elicitation|ElicitationResult)\"'` and the shared base `grep -oF 'return{session_id:e.id,transcript_path:lm(e.id),cwd:n'`.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.3",
"source": "measured now; SKILL.md elicitation-marker hook recipe"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -oE 'hook_event_name:\"(PreToolUse|PostToolUse|Elicitation|ElicitationResult)\"' ; strings -n 40 \"$bin\" | grep -oF 'return{session_id:e.id,transcript_path:lm(e.id),cwd:n' ; for k in hook_event_name tool_name tool_input session_id transcript_path tool_use_id elicitation_id mcp_server_name; do echo \"$k bare=$(strings -n 6 \"$bin\" | grep -oF \"$k\" | wc -l) quoted=$(strings -n 6 \"$bin\" | grep -oF \"\\\"$k\\\"\" | wc -l)\"; done ; python3 -c 'import json,glob;rows=[json.loads(l) for f in glob.glob(\"*/*/elicitations.jsonl\") for l in open(f) if l.strip()];print(len(rows))' (run in ~/.claude/projects)",
"observed": "Four payload constructors found verbatim: `{...Sa(o.session,ne(),d,o),hook_event_name:\"PreToolUse\",tool_name:e,tool_input:r,tool_use_id:n}`; `{...Sa(d.session,ne(),f,d),hook_event_name:\"PostToolUse\",tool_name:e,tool_input:r,tool_response:o,tool_use_id:n,duration_ms:C}`; `{...Sa(e,ne(),d),hook_event_name:\"Elicitation\",mcp_server_name:n,message:r,mode:v,url:C,elicitation_id:I,requested_schema:o}`; `{...Sa(e,ne(),d),hook_event_name:\"ElicitationResult\",mcp_server_name:n,elicitation_id:C,mode:v,action:r,content:o}`. The shared base returns `{session_id:e.id,transcript_path:lm(e.id),cwd:n,scratchpad_dir:...,prompt_id:...,permission_mode:r,agent_id:...,agent_type:d,effort:v}`. String-table tallies at 2.1.258, bare vs quoted: hook_event_name 91/0, tool_name 255/20, tool_input 107/10, session_id 507/12, transcript_path 23/1, tool_use_id 836/18, elicitation_id 12/0, mcp_server_name 36/3. Live end-to-end: 260 sidecar lines across the corpus, 256 carrying a csiftKey that starts `toolu_`, kinds AskUserQuestion 235 / ExitPlanMode 21 / unreadable-fossil 4; the newest pair (pending + resolved, 2026-09-02T08:20:36Z and T08:24:40Z) sits in a session whose records at that instant are stamped version 2.1.258.",
"rule": "Binary: one payload constructor per distinct `hook_event_name:\"<Event>\"` literal in the minified source. String tallies: `grep -oF` counts OCCURRENCES, and the bare and quoted forms are DIFFERENT counts - two of the eight keys (hook_event_name, elicitation_id) never appear in the quoted form at all, so a quoted-only rule reads them as absent. Corpus: one sidecar line per `elicitations.jsonl` line that parses as JSON; a csiftKey beginning `toolu_` proves the hook read a real `tool_use_id` off its stdin, and the file's existence at the transcript-derived path proves it read `transcript_path`/`session_id`.",
"note": "The behavior is confirmed twice over - statically in the four payload constructors, and live in sidecar lines written under 2.1.258 today. Only the instrument's counting rule and the tallies it produced needed fixing. csift's four code sites are verbatim at the claimed lines: SKILL.md:566, SKILL.md:575-576, src/model/record.rs:209-213, src/session/rows.rs:68-73."
}
]
},
{
"id": "ELI-020",
"area": "elicitation",
"behavior": "Claude Code 2.1.258 constructs a `type:\"system\"` / `subtype:\"hook_response\"` message with exactly the claimed fields, but (a) it is emitted only for hook events in `[\"SessionStart\",\"Setup\"]` unless a session-wide `allHookEventsEnabled` flag is set, and (b) it is NOT a transcript record: zero such lines exist in any transcript jsonl. It belongs to the same family as its siblings `hook_started` and `hook_progress`, which are likewise absent from disk - a live message stream, not persisted state.",
"depends": "csift's gated `harness.meta.system` leaf DOES classify such a record (verified against a synthetic fixture), but the record has no top-level `content`, so the leaf's `[<subtype> <level>] <content>` render produces `[hook_response]` with empty text: the hook's stdout/stderr are not matchable through it, and `show --line` shows the same empty excerpt. Only `--raw` would reach the bytes. And since no such record is ever written, csift cannot use it as evidence a hook ran; the on-disk trace of a hook that ran is the `hook_additional_context` attachment (leaf `harness.meta.hook`) when the hook injected context, and nothing at all otherwise.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "16-25",
"snippet": " \"system\" => match self.subtype.as_deref()? {\n \"turn_duration\" => Some(Class::MetaTurnDuration),\n \"away_summary\" => Some(Class::MetaAwaySummary),\n \"stop_hook_summary\" => Some(Class::MetaStopHooks),\n // The compaction boundary has its own leaf (`classify`, D7); every OTHER\n // system subtype is the harness talking to its own UI (v0.10.1 catch-all:\n // informational, api_error, model_refusal_*, agents_killed, local_command,\n // scheduled_task_fire, and whatever a later build adds).\n \"compact_boundary\" => None,\n _ => Some(Class::MetaSystem),"
},
{
"path": "src/model/taxonomy.rs",
"lines": "145-152",
"snippet": " /// `harness.meta.system` (v0.10.1) - the catch-all for every OTHER `type:\"system\"`\n /// subtype the harness writes for its own UI and never sends to the model:\n /// `informational` (e.g. the Remote Control disconnect warning, `level:\"warning\"`),\n /// `api_error`, `model_refusal_fallback` / `model_refusal_no_fallback`,\n /// `agents_killed`, `local_command`, `scheduled_task_fire`, and any subtype a\n /// future build adds. Renders `[<subtype> <level>] <content>`. No `message{}`, so\n /// invisible by the same instrument as the rest of this family; gated like them.\n MetaSystem,"
}
],
"instrument": "`csift search \"\" <target> -t harness.meta.system --count-by label` (the leaf is gated: without an explicit `-t` reaching it those lines are never parsed), then address one hit with `csift show <target> --line <n> --raw | jq 'keys, .subtype'`. Counting rule: one record per jsonl line whose `type` is `system` and `subtype` is `hook_response`. Binary cross-check: resolve the Claude Code binary (`bin=$(command -v claude)`; follow symlinks with `readlink -f`) and `strings -n 40 \"$bin\" | grep -F 'subtype:\"hook_response\"' | head -1` prints the single minified record constructor listing the whole field set.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.1",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -cF 'subtype:\"hook_response\"' ; strings -n 40 \"$bin\" | grep -oF 'var J2r=[\"SessionStart\",\"Setup\"]' ; cd ~/.claude/projects && rg -g '*.jsonl' -o '\"subtype\":\"hook_response\"' . | wc -l ; rg -g '*.jsonl' -o '\"subtype\":\"[a-z_]+\"' . | sed 's/.*subtype...//;s/\"$//' | sort | uniq -c | sort -rn ; plus a synthetic fixture under --claude-home carrying one `type:\"system\"`/`subtype:\"hook_response\"` record with the exact field set, scanned by the in-tree csift build.",
"observed": "Binary: exactly 1 constructor, `_u({type:\"system\",subtype:\"hook_response\",hook_id:e.hookId,hook_name:e.hookName,hook_event:e.hookEvent,output:e.output,stdout:e.stdout,stderr:e.stderr,...e.exitCode!==void 0&&{exit_code:e.exitCode},outcome:e.outcome})` - the claimed field set exactly. But it sits behind `if(!xSe(e.hookEvent))return;` where `function xSe(e){if(J2r.includes(e))return!0;return $5t().allHookEventsEnabled&&Hh.includes(e)}` and `var J2r=[\"SessionStart\",\"Setup\"]`. Corpus: `\"subtype\":\"hook_response\"` occurs 0 times across every *.jsonl under ~/.claude/projects. The system subtypes that DO occur, with counts: stop_hook_summary 5656, turn_duration 4644, away_summary 1636, scheduled_task_fire 459, compact_boundary 232, model_refusal_fallback 51, api_error 29, local_command 15, agents_killed 10, informational 5, model_refusal_no_fallback 2 - eleven subtypes, none of them hook_response, hook_started or hook_progress. Fixture: with the exact harness field set (no top-level `content`), csift's gated leaf classifies and censuses the record (`-t harness.meta.system --count-by label` -> 1) and renders `harness.meta.system L2 [hook_response]` with an EMPTY excerpt, so `search \"OUTPUTMARKER\" -t harness.meta.system` returns 0 while a bare scan returns 0 as designed; adding a top-level `content` field makes it matchable.",
"rule": "Binary: one tally per exact `subtype:\"hook_response\"` byte sequence in the string table. Corpus: one record per jsonl line whose `subtype` value is the named string, counted over every *.jsonl under ~/.claude/projects (the `-g '*.jsonl'` glob matters - the same token appears 101 times inside message text and inside externalised tool-result .txt files, which are not records). Fixture: one record per line of a two-line synthetic transcript under `--claude-home`.",
"note": "The claim's strongest consequence - 'the only on-disk evidence that a hook recipe ran at all and what it printed' - is refuted: there is no such on-disk evidence, at 2.1.258 or in any version present in this corpus. A future build that flips allHookEventsEnabled and persists the stream would be caught by re-running the corpus census above. csift's two code sites are verbatim at the claimed lines: src/model/classify_promoted.rs:16-25 and src/model/taxonomy.rs:145-152."
}
]
},
{
"id": "ELI-021",
"area": "elicitation",
"behavior": "A hook's stdout `additionalContext` is injected ONLY when the returned JSON nests it under `hookSpecificOutput` together with a `hookEventName` matching the firing event; a bare top-level `additionalContext` is discarded, and Claude Code's own diagnostic for that shape is the literal `Did you mean hookSpecificOutput.additionalContext (with a hookEventName)?`.",
"depends": "Dropping the `hookEventName` echo is not a fully silent no-op, and the two ways to get it wrong differ. Omitting the nesting entirely (a bare top-level `additionalContext`) is ignored WITH a logged diagnostic naming the fix. Nesting it but echoing the WRONG event name THROWS: `Hook returned incorrect event name: expected '<Event>' but got '<Event>'. Full stdout: ...`. Also, neither path records a `hook_response` transcript record - see ELI-020 - so the phrase 'still records its hook_response' should be struck.",
"code": [
{
"path": "SKILL.md",
"lines": "532",
"snippet": "$chunk\" '{hookSpecificOutput:{hookEventName:\"SessionStart\",additionalContext:$c}}'"
},
{
"path": "SKILL.md",
"lines": "554",
"snippet": "jq -n --arg c \"$ctx\" '{hookSpecificOutput:{hookEventName:\"PostToolUseFailure\",additionalContext:$c}}'"
}
],
"instrument": "resolve the Claude Code binary (`bin=$(command -v claude)`; follow symlinks with `readlink -f`) and `strings -n 40 \"$bin\" | grep -F 'with a hookEventNa'`: the literal `Did you mean hookSpecificOutput.additionalContext (with a hookEventName)?` must be present. Counting rule: exact literal presence in the string table (one match). Live check: return the bare top-level form from a `SessionStart` hook and confirm `csift search \"\" @<session> -t harness.meta.hook --additional-context -c` stays 0, then re-run with the nested form and confirm it becomes 1.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now; SKILL.md hook recipes 1-2"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -cF 'Did you mean hookSpecificOutput.additionalContext (with a hookEventName)?' ; strings -n 40 \"$bin\" | grep -oF 'Hook JSON output had unrecognized keys (ignored): ' ; strings -n 40 \"$bin\" | grep -oF 'if(f&&e.hookSpecificOutput.hookEventName!==f)throw Error' ; csift search \"\" . -t harness.meta.hook --additional-context --count-by session",
"observed": "The literal `Did you mean hookSpecificOutput.additionalContext (with a hookEventName)?` is present (2 occurrences: the string-table blob and the code site). Its function walks the returned object's keys against the accepted schema, collects the unrecognized ones, and logs `Hook JSON output had unrecognized keys (ignored): <keys>.` appending that hint only when `additionalContext` is among them - so a bare top-level `additionalContext` is dropped as an unrecognized key. Separately, the accepted path is gated: `if(e.hookSpecificOutput){if(f&&e.hookSpecificOutput.hookEventName!==f)throw Error(\\`Hook returned incorrect event name: expected '${f}' but got '${e.hookSpecificOutput.hookEventName}'...\\`);switch(e.hookSpecificOutput.hookEventName){...}}`. Live: with the nested form in use, `csift search \"\" . -t harness.meta.hook --additional-context --count-by session` reports 9961 matched records across 353 session keys.",
"rule": "Binary: exact literal presence in the string table (grep -cF), one match each for the diagnostic, the unrecognized-keys prefix, and the event-name equality throw. Corpus: one census record per `type:\"attachment\"` record whose payload type is `hook_additional_context`, counted per owning session by csift's `--count-by session` axis.",
"note": "The behavior statement is corroborated verbatim; only the depends narrative needed tightening. csift's two recipe sites are verbatim at the claimed lines: SKILL.md:532 and SKILL.md:554, both emitting `{hookSpecificOutput:{hookEventName:\"<Event>\",additionalContext:$c}}` with the event echoed."
}
]
},
{
"id": "ELI-022",
"area": "elicitation",
"behavior": "TWELVE hook events read `hookSpecificOutput.additionalContext` and inject it: PostModelSwitch, PostToolBatch, PostToolUse, PostToolUseFailure, PreToolUse, SessionStart, Setup, Stop, SubagentStart, SubagentStop, UserPromptExpansion, UserPromptSubmit. Every other event runs its hook and discards the field.",
"depends": "csift's shipped recipes are registered under events from this list (`SessionStart(compact)` for the verbatim re-injection, `PostToolUseFailure(TaskStop)` for the teammate-kill redirect); one registered under any OTHER event runs, exits 0, and is silently ignored. What an injection leaves on disk is a `type:\"attachment\"` record whose payload is `{\"type\":\"hook_additional_context\",\"content\":[...]}` - csift labels it `harness.meta.hook` and parses it only under `search --additional-context` or an explicit `show` address, so the trace exists but never pollutes a default scan.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "288-341",
"snippet": " #[must_use]\n pub fn hook_additional_context_text(&self) -> Option<String> {\n if !self.is_type(\"attachment\") {\n return None;\n }\n let v = self.attachment_value()?;\n let att = v.as_object()?;"
},
{
"path": "src/model/classify.rs",
"lines": "150-155",
"snippet": " // Hook-injected additionalContext (a `type:\"attachment\"` record): harness machinery,\n // not a message - labeled `harness.meta.hook`. Only `search --additional-context`\n // (or an explicit `show --line`/`--uuid` address) ever parses these lines, so the\n // label is unreachable elsewhere; the record never opens a turn.\n if self.hook_additional_context_text().is_some() {\n push_unique(&mut out, Class::MetaHook);"
},
{
"path": "SKILL.md",
"lines": "532",
"snippet": "$chunk\" '{hookSpecificOutput:{hookEventName:\"SessionStart\",additionalContext:$c}}'"
},
{
"path": "SKILL.md",
"lines": "554",
"snippet": "jq -n --arg c \"$ctx\" '{hookSpecificOutput:{hookEventName:\"PostToolUseFailure\",additionalContext:$c}}'"
}
],
"instrument": "The claim's regex `case\"[A-Za-z]+\":(if\\()?F\\.additionalContext` structurally cannot see two of the twelve: PreToolUse assigns after a nested permissionDecision switch, and Stop is a bare fallthrough label sharing SubagentStop's assignment. Use the brace-depth arm scan above, or cross-check against the on-disk `attachment.hookEvent` census, which surfaces PreToolUse immediately as the largest bucket.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" into a scratch file, then a python brace-depth scan that locates `switch(e.hookSpecificOutput.hookEventName){`, walks braces to the matching close, splits the body on `case\"X\":` labels found at brace depth 0, and reports every label whose arm (fallthrough runs inherited) mentions `additionalContext`. Corpus side: python over ~/.claude/projects/*/*.jsonl tallying `attachment.hookEvent` for every record whose `attachment.type` is `hook_additional_context`.",
"observed": "The switch has 18 top-level case arms: PreToolUse, UserPromptSubmit, UserPromptExpansion, SessionStart, Setup, PreModelSwitch, PostModelSwitch, SubagentStart, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, PermissionDenied, PermissionRequest, Elicitation, ElicitationResult, MessageDisplay. TWELVE of them assign additionalContext: PostModelSwitch, PostToolBatch, PostToolUse, PostToolUseFailure, PreToolUse, SessionStart, Setup, Stop, SubagentStart, SubagentStop, UserPromptExpansion, UserPromptSubmit. The two the claim missed are visible verbatim: PreToolUse's arm ends `...if(F.hookPermissionDecisionReason=...,e.hookSpecificOutput.updatedInput)F.updatedInput=...;F.additionalContext=e.hookSpecificOutput.additionalContext;break;`, and Stop is a fallthrough sharing the assignment: `case\"Stop\":case\"SubagentStop\":F.additionalContext=e.hookSpecificOutput.additionalContext;break;`. Corpus: 80756 `hook_additional_context` attachment records, by hookEvent - PreToolUse 50074, PostToolUse 26900, UserPromptSubmit 2783, PostToolUseFailure 688, SessionStart 311. PreToolUse is thus the dominant on-disk carrier, 62% of all injections.",
"rule": "Binary: one event per case label in the additionalContext switch whose arm assigns `additionalContext`, with fallthrough label runs credited to the arm they fall into and nested switches skipped by brace depth. Corpus: one record per jsonl line whose `attachment.type` is `hook_additional_context`, bucketed by its `attachment.hookEvent` field.",
"note": "The mechanism, the attachment trace and csift's gating all hold; only the enumeration was wrong, and wrong in a way that omitted the majority carrier. csift's four code sites are verbatim at the claimed lines: src/model/predicates.rs:335-341, src/model/classify.rs:150-155, SKILL.md:532, SKILL.md:554."
}
]
},
{
"id": "ELI-023",
"area": "elicitation",
"behavior": "A hook matcher is consulted for 21 hook events, not two, and what it is tested against varies by event: the tool name for the five tool/permission events, `source` for SessionStart, `trigger` for Setup and the compaction events, `agent_type` for the subagent events, and - for the two MCP elicitation events - `mcp_server_name`. So Elicitation and ElicitationResult DO take a matcher; it selects which MCP server the hook fires for. The grammar is `/^[a-zA-Z0-9_|, -]+$/` split on `/[|,]/` with each part trimmed for events in the lenient set (which includes both elicitation events); events outside it use the stricter `/^[a-zA-Z0-9_|]+$/` and split on `|` only, with no comma and no space. An absent or empty matcher matches everything, and so do `*` AND `.*`; an event whose matched field is undefined also always matches.",
"depends": "csift's registration remains correct in practice for a different reason than stated: registering Elicitation and ElicitationResult with NO matcher matches every MCP server, which is what the recipe wants. The claim that those events 'take no matcher at all' is what is wrong.",
"code": [
{
"path": "SKILL.md",
"lines": "598",
"snippet": "Register 5 events (one script, absolute path): `PreToolUse`+`PostToolUse`+`PostToolUseFailure` matcher `\"AskUserQuestion|ExitPlanMode\"`; `Elicitation`+`ElicitationResult` (no matcher)."
},
{
"path": "src/session/rows.rs",
"lines": "68-73",
"snippet": " /// True when the session's elicitation SIDECAR FILE exists at all (= the csift hook\n /// is installed for this session - resolved pairs stay in the file). The tri-state a\n /// consumer needs: present+pending / present+none (safe to conclude \"not blocked on\n /// an elicitation\") / absent (hook unknown - CANNOT conclude anything). Always false\n /// for a subagent row (the sidecar is keyed by the top-level session).\n pub sidecar_present: bool,"
},
{
"path": "src/elicitation.rs",
"lines": "91-93",
"snippet": "/// MCP markers are exempt: an MCP elicitation never has a native form, so sidecar pairing\n/// stays its only signal - and its `csiftKey` may be a non-unique server name, unsafe to\n/// substring-scan."
}
],
"instrument": "resolve the Claude Code binary (`bin=$(command -v claude)`; follow symlinks with `readlink -f`) and `strings -n 40 \"$bin\" | grep -oE 'if\\(e!==\"PostToolUse\"&&e!==\"PostToolUseFailure\"\\)return!1;.{0,200}'`: the early return is followed by the `/^[a-zA-Z0-9_|, -]+$/` test and the `.split(/[|,]/)` with a trim per part. Counting rule: exact literal presence of the regex and of the split expression (one match each). Live check: register the elicitation recipe with a matcher containing a colon and confirm the sidecar is never created.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now; SKILL.md elicitation-marker hook registration"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -oE 'if\\(e!==\"PostToolUse\"&&e!==\"PostToolUseFailure\"\\)return!1;.{0,200}' ; strings -n 40 \"$bin\" | grep -oF 'case\"Elicitation\":return e.mcp_server_name' ; strings -n 40 \"$bin\" | grep -oE 'matcherMetadata:\\{fieldToMatch:\"[a-z_]+\"' | sort | uniq -c ; strings -n 40 \"$bin\" | grep -oF '.split(/[|,]/).map((d)=>d.trim()).filter(Boolean)'",
"observed": "The runtime matcher is `function hXn(e,n,r){let o=_Xn(e);return!o||!n||Kwt(o,xnr(e.hook_event_name,n),Wje.has(e.hook_event_name),r,void 0,\"tool_input\"in e?e.tool_input:void 0)}` and `_Xn` is a switch that returns the value the matcher tests against, PER EVENT: `tool_name` for PreToolUse/PostToolUse/PostToolUseFailure/PermissionRequest/PermissionDenied; `command_name` for UserPromptExpansion; `source` for SessionStart/ConfigChange/DirectoryAdded; `trigger` for Setup/PreCompact/PostCompact; `notification_type` for Notification; `reason` for SessionEnd; `error` for StopFailure; `agent_type` for SubagentStart/SubagentStop; `load_reason` for InstructionsLoaded; a path derivation for FileChanged; and, verbatim, `case\"Elicitation\":return e.mcp_server_name;case\"ElicitationResult\":return e.mcp_server_name;`. The declared registry agrees: 22 `matcherMetadata:{fieldToMatch:...}` entries, of which `tool_name` 5, `source` 3, `trigger` 3, `agent_type` 2, `to_model` 2, `mcp_server_name` 2, and one each for `command_name`, `error`, `load_reason`, `notification_type`, `reason`. `Wje` - the set that selects the LENIENT grammar - holds 21 events including Elicitation and ElicitationResult. The grammar itself is real but two-valued: `function Cnr(e,n,r){if(!(n?/^[a-zA-Z0-9_|, -]+$/:/^[a-zA-Z0-9_|]+$/).test(e))return;return e.split(n?/[|,]/:\"|\").map((d)=>d.trim()).filter(Boolean)...}`. Match-all is `function mEe(e){return!e||e===\"*\"||e===\".*\"}` - three forms, not two. The function the claim cited, `if(e!==\"PostToolUse\"&&e!==\"PostToolUseFailure\")return!1;`, is a NARROW helper: its body then tests whether every tool the matcher names is in `[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]`, i.e. 'does this matcher select only file-writing tools', not 'does this matcher fire'.",
"rule": "One matched-field mapping per `case\"<Event>\":return e.<field>` arm of the runtime field switch; one declared matcher per `matcherMetadata:{fieldToMatch:\"<field>\"` entry in the event registry; exact literal presence for the two grammar regexes and the match-all predicate.",
"note": "Live corroboration that the grammar half is real: the elicitation recipe is registered with matcher \"AskUserQuestion|ExitPlanMode\" on the three tool events and no matcher on the two MCP events, and it has written 260 sidecar lines across the corpus (235 AskUserQuestion, 21 ExitPlanMode, 4 unreadable fossils), the newest pair under 2.1.258. csift's two code sites are verbatim at the claimed lines: SKILL.md:598 and src/session/rows.rs:68-73. code-site note: The claim's cited function is the wrong one. The matcher dispatch to cite is the pair `hXn` (the test) and `_Xn` (the per-event matched-field switch), reachable with `strings -n 40 \"$bin\" | grep -oF 'case\"Elicitation\":return e.mcp_server_name'`."
}
]
},
{
"id": "ELI-024",
"area": "elicitation",
"behavior": "`CLAUDE_PROJECT_DIR` is set in the environment of EVERY hook child process at 2.1.258, with no distinction between a user-level and a project-level registration, and `${CLAUDE_PROJECT_DIR}` is substituted literally into exec-form command and args regardless of scope. A shell-form user-level hook can therefore expand it normally. The variables that genuinely are scope-gated are `${CLAUDE_PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_DATA}`, which throw a named error when used outside a plugin registration. A relative `script`-type hook path is anchored to `${CLAUDE_PROJECT_DIR}/<path>` rather than failing.",
"depends": "Absolute script paths in csift's recipes remain good practice - they make the registration independent of which project the session opened in, which is a real hazard when one global hook serves many repositories - but the stated REASON (no CLAUDE_PROJECT_DIR for global installs) does not hold at 2.1.258, so a relative path in a global registration does not fail for that reason.",
"code": [
{
"path": "SKILL.md",
"lines": "534",
"snippet": "Register N times: `{\"matcher\":\"compact\",\"hooks\":[{\"type\":\"command\",\"command\":\"/ABS/csift-turns-slice.sh i\"}]}`, i=1..N. Absolute path (a relative command resolves against the hook child's cwd, not the skill's install dir; `$CLAUDE_PROJECT_DIR` is set for every hook child, global registrations included, so it is not the reason); `--window 9000` stays under the 10K additionalContext cap."
},
{
"path": "SKILL.md",
"lines": "598",
"snippet": "Register 5 events (one script, absolute path): `PreToolUse`+`PostToolUse`+`PostToolUseFailure` matcher `\"AskUserQuestion|ExitPlanMode\"`; `Elicitation`+`ElicitationResult` (no matcher)."
}
],
"instrument": "Quote the counting rule: `strings -n 10 \"$bin\" | grep -c 'CLAUDE_PROJECT_DIR'` = 29 LINES at 2.1.258; occurrences are 46. Neither number tests the claim - the deciding instrument is `grep -oF 'CLAUDE_PROJECT_DIR:Ue(je)'` and `grep -oF 'CLAUDE_PROJECT_DIR:n.projectDir'`, showing the ungated env injection.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SKILL.md hook recipe 1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 10 \"$bin\" | grep -c 'CLAUDE_PROJECT_DIR' ; strings -n 10 \"$bin\" | grep -o 'CLAUDE_PROJECT_DIR' | wc -l ; strings -n 40 \"$bin\" | grep -oF 'CLAUDE_PROJECT_DIR:Ue(je)' ; strings -n 40 \"$bin\" | grep -oF 'CLAUDE_PROJECT_DIR:n.projectDir' ; strings -n 40 \"$bin\" | grep -oF 'but the hook is not associated with a plugin. This variable is only available in hooks defined in a plugin'",
"observed": "grep -c reports 29 and grep -o | wc -l reports 46 - the claim's 29 is a LINE count, not an occurrence count. In the single hook executor, `je=f.projectRoot` and the environment handed to every hook child is built as `gn={...,CLAUDE_PROJECT_DIR:Ue(je)}`; the fallback spawn path likewise passes `env:{...,CLAUDE_PROJECT_DIR:n.projectDir}`. Neither site branches on where the hook was registered. Exec-form commands additionally get literal substitution: `Ps=Ps.replaceAll(\"${CLAUDE_PROJECT_DIR}\",()=>je)` applied to `e.command` and every arg, again ungated. The ONLY scope-gated variables are the plugin ones, and the harness says so explicitly: `Hook command references ${CLAUDE_PLUGIN_ROOT} but the hook is not associated with a plugin. This variable is only available in hooks defined in a plugin's hooks/hooks.json file, not in settings.json.` - there is no counterpart message for CLAUDE_PROJECT_DIR. A `script`-type hook with a relative path is not rejected but ANCHORED: `if(isAbsolute(e)||[\"${CLAUDE_PROJECT_DIR}\",\"${CLAUDE_PLUGIN_ROOT}\",\"${CLAUDE_PLUGIN_DATA}\"].some(r=>e.startsWith(r)))return e; if(e===\"~\"||/^~[\\\\/]/.test(e))return join(homedir(),e.slice(2)); return `${CLAUDE_PROJECT_DIR}/${e}``.",
"rule": "grep -c counts matching LINES of the strings output; grep -o | wc -l counts occurrences - the two differ 29 vs 46 here, so a claim quoting one must name which. For the mechanism: one hook-executor env construction site and one fallback spawn site, both inspected for a registration-scope branch; absence of any such branch, plus the presence of an explicit scope error for the plugin variables and none for CLAUDE_PROJECT_DIR, is the finding.",
"note": "What is still open: whether a shell-form relative command path resolves, which depends on the hook child's cwd rather than on CLAUDE_PROJECT_DIR. Deciding it needs the live re-registration test the claim describes (register a global hook with a relative command, fire its event, look for a `harness.meta.hook` attachment); it was not run here because it would mutate the operator's global hook registrations. csift's two code sites are verbatim at the claimed lines: SKILL.md:534 and SKILL.md:598."
}
]
},
{
"id": "ELI-025",
"area": "elicitation",
"behavior": "10000 characters is a THRESHOLD, not a hard cap, and crossing it does not silently truncate. Above it the harness persists the payload to a `tool-results/hook-<id>-<n>-additionalContext.txt` file beside the transcript and injects a `<persisted-output>` pointer carrying the true size and a preview; only if persisting fails does it truncate, and then it appends `[Hook additionalContext truncated at 10000 chars - persist-to-disk failed: <error>]`. Both outcomes are self-disclosing. Separately, the SessionStart carrier is observed delivering payloads far above the threshold intact - 32127 characters at 2.1.258.",
"depends": "The failure mode the claim names - 'exceeding the cap re-injects clipped turns as if whole' - does not occur at 2.1.258: neither over-threshold path is silent. `verbatim --window 9000` remains a reasonable default (it keeps each slice inside the threshold and so avoids the pointer indirection entirely), but it is a tidiness choice, not a fidelity guard.",
"code": [
{
"path": "SKILL.md",
"lines": "372",
"snippet": "- Slicing (hook injection, ≤10000-char cap): `--slices N --slice i --window W` = fixed-fleet chunks, whole turns; out-of-range ⇒ nothing, exit 0 (hook affordance). Text-only."
},
{
"path": "SKILL.md",
"lines": "534",
"snippet": "Register N times: `{\"matcher\":\"compact\",\"hooks\":[{\"type\":\"command\",\"command\":\"/ABS/csift-turns-slice.sh i\"}]}`, i=1..N. Absolute path (a relative command resolves against the hook child's cwd, not the skill's install dir; `$CLAUDE_PROJECT_DIR` is set for every hook child, global registrations included, so it is not the reason); `--window 9000` stays under the 10K additionalContext cap."
},
{
"path": "src/turns/run.rs",
"lines": "49-53",
"snippet": " // Normalize the budget to characters. `--slices N` pins the FLEET size, so the budget is\n // derived as N windows (the slice COUNT is the hard constraint - a fixed set of registered\n // hooks must never need to grow); otherwise it is the requested char/token amount.\n let budget_chars = if let Some(n) = args.slices {\n n.saturating_mul(args.window)"
},
{
"path": "src/cli/verbatim_whoami_args.rs",
"lines": "285-288",
"snippet": " /// fanning a >10K reconstruction across several SessionStart hooks: Claude Code caps EACH\n /// hook's `additionalContext` at 10,000 CHARACTERS (over-cap is replaced by a file-path +\n /// short preview, i.e. the body is effectively LOST to the model), so one hook per slice\n /// keeps every injected chunk under the wall. Slicing is DETERMINISTIC (same session +"
},
{
"path": "src/turns/render.rs",
"lines": "49-51",
"snippet": " let cap_override = slices.map(|_| window.saturating_sub(SLICE_BODY_HEADROOM).max(1));\n let doc = build_document_body(sessions, plans, &ctx.cfg, cap_override);\n let chunks = slice_into_windows(&doc, window);"
}
],
"instrument": "Return an `additionalContext` of exactly 10,001 characters from a `SessionStart` hook, then read what landed: `csift search \"\" @<session> -t harness.meta.hook --additional-context --format json --no-truncate | jq '.text|length'`. Counting rule: characters of the injected attachment payload compared against the characters the hook printed; a shortfall at 10,000 is the cap. A static corpus cannot settle this - it needs a live hook.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "SKILL.md hook recipe 1; SKILL.md verbatim slicing"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -oF 'NQn=1e4' ; strings -n 40 \"$bin\" | grep -oF 'async function ipe(e,n,r,{threshold:o=NQn,storageV5:d}={}){if(e.length<=o)return e;' ; strings -n 40 \"$bin\" | grep -oF 'yield{additionalContexts:[await ipe(Rn.additionalContext,`${o}-${Bn}`,\"additionalContext\",{storageV5:we})]}' ; plus a python pass over one project directory's *.jsonl measuring `len(attachment.content)` for every `hook_additional_context` record, bucketed by the record's `version` field.",
"observed": "The constant is real: `NQn=1e4`, and it is the default `threshold` of the handler every injected additionalContext passes through. But the over-threshold branch is NOT truncation: `if(e.length<=o)return e; let f=await $3(e,\\`hook-${n}-${r}\\`,kS(),d); if(U3(f))return ...,\\`${e.slice(0,o)}\\n[Hook ${r} truncated at ${o} chars - persist-to-disk failed: ${f.error}]\\`; let _=Npe(f); return ...,_;` - the payload is persisted to a sibling file and replaced by a pointer, and truncation is only the persist-FAILURE fallback, carrying an explicit marker naming the char count and the reason. On disk both halves are visible. Ten records carry the pointer form, e.g. `<persisted-output>\\nOutput too large (12.3KB). Full output saved to: <project>/<session>/tool-results/hook-<tool_use_id>-<n>-additionalContext.txt` followed by a preview - and the pointer record itself is ~2.2KB, i.e. the model saw the pointer, not a silent stub. And the cap is not universal: of 9459 hook_additional_context records in one project directory, 17 exceed 10000 chars (max 41398), and the single record stamped version 2.1.258 among them is a SessionStart injection of 32127 chars that landed WHOLE - no persisted pointer, no truncation marker.",
"rule": "Binary: exact literal presence of the constant, the handler signature and the injection call site. Corpus: one measurement per `type:\"attachment\"` record whose payload type is `hook_additional_context`, length = characters of the payload content (list blocks joined with a newline), attributed to the CC build by the record's own `version` field; 'landed whole' = length above 10000 with neither the substring `persisted-output` nor `truncated at` present.",
"note": "The claim's own instrument (return exactly 10001 characters and measure what landed) would still be the cleanest confirmation, but it is no longer needed: the corpus already contains the over-threshold cases in both forms, including one under the current build. csift's three code sites are verbatim at the claimed lines: SKILL.md:372, SKILL.md:534, src/turns/run.rs:49-53 - note that SKILL.md:372 and :534 both describe the threshold as a hard 10K cap and would need rewording to match the persist-then-pointer behavior."
}
]
},
{
"id": "FH-001",
"area": "freshness-file-history",
"behavior": "Claude Code writes a `type:\"file-history-snapshot\"` line carrying exactly the top-level keys `type`, `messageId`, `snapshot` and `isSnapshotUpdate` - no top-level `timestamp`, `uuid` or `parentUuid` - and nests `{messageId, timestamp, trackedFileBackups}` inside `snapshot`, so the line's instant must be read from `snapshot.timestamp` (or a per-entry `backupTime`).",
"depends": "csift models it as a timestamp-less metadata line, promotes it to the gated leaf `harness.meta.snapshot`, and reads the instant out of `snapshot.timestamp` (falling back to the record timestamp): reading a top-level timestamp yields nothing, which would place every `recover` snapshot event at an unknown time and drop the record from any time window.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "265-268",
"snippet": "/// The `harness.meta.snapshot` excerpt for a `file-history-snapshot` (every tracked\n/// path with its version, sorted) or a `file-history-delta` (the one path). The\n/// snapshot line has no top-level timestamp; the nested one rides the excerpt.\nfn snapshot_record_text(rec: &Record) -> Option<String> {"
},
{
"path": "src/search/record_text.rs",
"lines": "234-290",
"snippet": " }\n let snap = rec.snapshot.as_ref()?;\n let at = snap\n .get(\"timestamp\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or(\"?\");"
}
],
"instrument": "`rg -m1 '\"type\":\"file-history-snapshot\"' <transcript> | jq 'has(\"timestamp\"), .snapshot.timestamp'` must print `false` then an ISO instant; then union the top-level keys of every such line across `~/.claude/projects` and expect exactly those four. Counting rule: one key set per snapshot line, unioned per file.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md sections 4.8 and 5.1; CHANGELOG 0.10.0; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search '' -t harness.meta.snapshot --raw --max-count 0 > snap_raw.jsonl; rg -N '\"type\":\"file-history-snapshot\"' snap_raw.jsonl | jq -c 'keys' | sort | uniq -c; rg -N '\"type\":\"file-history-snapshot\"' snap_raw.jsonl | jq -c '.snapshot|keys' | sort | uniq -c; rg -N -m1 '\"type\":\"file-history-snapshot\"' <CORPUS>/<SESSION>.jsonl | jq -c 'has(\"timestamp\"), has(\"uuid\"), has(\"parentUuid\"), .snapshot.timestamp'; csift search '' @<SESSION> -t harness.meta.snapshot --max-count 2 --format json | jq -c 'select(.kind==\"exchange\")|.hits[0]|{line,label,ts_utc,excerpt}'",
"observed": "3947 of 3947 snapshot lines carry the key set [\"isSnapshotUpdate\",\"messageId\",\"snapshot\",\"type\"] and nothing else; 3947 of 3947 nest exactly [\"messageId\",\"timestamp\",\"trackedFileBackups\"]; the single-line probe printed false, false, false then \"2026-08-12T00:57:24.517Z\"; snapshot.timestamp matched ^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*Z$ on 3947 of 3947. csift's own render agrees: the hit JSON carries \"ts_utc\":null with excerpt \"[file-history snapshot at 2026-08-12T00:57:24.517Z: ]\".",
"rule": "One key set per file-history-snapshot line, unioned over every such line csift emitted with --raw across all projects; presence tested with jq has() so a present-but-null key would still count as present.",
"note": "Confirmed unchanged at CC 2.1.258. The consequence the claim predicts is directly visible in csift's output: a snapshot hit's ts_utc/ts_local are null because there is no top-level timestamp, and the instant that reaches the reader comes from snapshot.timestamp inside the excerpt. src/recover/events.rs:196-200 still matches the pinned snippet verbatim; the two record_text.rs sites moved down 22 lines with byte-identical text."
}
]
},
{
"id": "FH-002",
"area": "freshness-file-history",
"behavior": "A snapshot's snapshot.trackedFileBackups maps each tracked path to exactly {backupFileName, version, backupTime} plus realParentDir, and the tracked path is spelled either absolute or relative. realParentDir is declared optional and IS absent from every entry written by older builds, but current Claude Code emits it on every entry: measured 0 of 1014 entries in a 2.1.207-only session against 1214 of 1214 in a 2.1.234-only session, 37 of 37 at 2.1.251 and 667 of 667 at 2.1.258. Corpus-wide across all builds present in the store: 90047 of 293298 entries (30.7 percent), paths 188048 absolute to 105250 relative.",
"depends": "`recover` reads `version`/`backupFileName`/`backupTime` off each entry and never assumes `realParentDir`; a relative tracked path with no `realParentDir` cannot be resolved from the record alone, which is why csift's settings-family scope law matches on path SHAPE rather than on a reconstructed absolute path.",
"code": [
{
"path": "src/model/record.rs",
"lines": "135-143",
"snippet": " /// `file-history-snapshot` payload (a top-level sibling). Carries\n /// `{messageId, trackedFileBackups: {<path>: {backupFileName, version, backupTime}}}`.\n /// Read only by `recover` to know a disk backup EXISTED for a path at a time\n /// (a coverage annotation). `backupFileName` is usually present (measured 83-98%\n /// across real corpora), but the store it names is PRUNED and its content has no\n /// transcript anchor, so it is never used to fabricate content; `recover\n /// --list-backups` lists the store itself. Additive + tolerant.\n #[serde(default)]\n pub snapshot: Option<serde_json::Value>,"
}
],
"instrument": "Iterate every `trackedFileBackups` entry of every snapshot line in a set of large transcripts and count presence of `realParentDir`, null `backupFileName`, and absolute-vs-relative path spelling. Counting rule: one entry per (snapshot line, tracked path) pair, not per line.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.1",
"source": "src/model/record.rs comment; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -N '\"type\":\"file-history-snapshot\"' snap_raw.jsonl | jq -c '.snapshot.trackedFileBackups | to_entries[] | {p:.key, k:(.value|keys), rp:(.value|has(\"realParentDir\")), bfn:.value.backupFileName}' > entries.jsonl; jq -c '.k' entries.jsonl | sort | uniq -c; jq -r '.rp' entries.jsonl | sort | uniq -c; jq -r '.p | if startswith(\"/\") then \"absolute\" else \"relative\" end' entries.jsonl | sort | uniq -c; then per session: for f in $(rg -lN '\"type\":\"file-history-snapshot\"' <CORPUS>/*.jsonl); do rg -N '\"type\":\"file-history-snapshot\"' $f | jq -r '[.snapshot.trackedFileBackups|to_entries[]|(.value|has(\"realParentDir\"))]|map(select(.))|length'; rg -oN '\"version\":\"2\\.1\\.[0-9]+\"' $f | sort -u; done",
"observed": "293298 entries. Entry key sets are exactly two: [\"backupFileName\",\"backupTime\",\"version\"] on 203251 and [\"backupFileName\",\"backupTime\",\"realParentDir\",\"version\"] on 90047 (30.7 percent), so no third shape exists. Paths: 188048 absolute, 105250 relative. Split by the CC version stamped on the session's records: 0 of 1014 entries carry realParentDir in a session whose records are all 2.1.207, 0 of 305 at 2.1.206, 0 of 627 at 2.1.191; 1214 of 1214 in a session whose records are all 2.1.234, 37 of 37 at 2.1.251, and 667 of 667 over the five newest snapshot lines of a live session whose tail records are 2.1.258.",
"rule": "One entry per (snapshot line, tracked path) pair, not per line. A session is assigned a version set by the distinct \"version\":\"2.1.N\" values appearing anywhere in its records; only single-version sessions were used for the boundary.",
"note": "The schema half holds verbatim: exactly two key shapes, never a third, and csift's Option handling is unaffected. What needs correcting is the 38 percent figure, which is a mixed-build aggregate and is not current-CC behavior. The version boundary lies between 2.1.208 and 2.1.234 and could not be narrowed further here because snapshot lines carry no version field of their own and the only sessions spanning that gap are multi-version. The practical consequence for the claim's depends clause: under current CC a relative tracked path always arrives with a realParentDir, so the unresolvable-relative-path case exists only in older transcripts, which csift still reads. src/recover/events.rs:209-220 matches verbatim."
}
]
},
{
"id": "FH-003",
"area": "freshness-file-history",
"behavior": "backupFileName is a key that is always emitted; its VALUE is null on 63492 of 293298 entries corpus-wide (21.6 percent), so 78.4 percent of entries name a store file. Per session the rate is 79-99 percent among sessions with at least 250 entries, and 91.8 percent (612 of 667) in the newest snapshot lines of a live 2.1.258 session. The doc's \"83-98% present\" band is slightly too narrow at the low end and the corpus figure sits just below it.",
"depends": "csift reads it only to know a disk backup EXISTED for a path at a time (a coverage annotation) and to key the store lookup; the store it names is pruned and its content has no transcript anchor, so it is never used to fabricate content.",
"code": [
{
"path": "src/model/record.rs",
"lines": "138-141",
"snippet": " /// (a coverage annotation). `backupFileName` is usually present (measured 83-98%\n /// across real corpora), but the store it names is PRUNED and its content has no\n /// transcript anchor, so it is never used to fabricate content; `recover\n /// --list-backups` lists the store itself. Additive + tolerant."
}
],
"instrument": "Count `trackedFileBackups` entries corpus-wide and, among them, how many carry a non-null `backupFileName`. Counting rule: one probe per tracked-path entry, presence reported as a percentage of entries.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.1",
"source": "CHANGELOG 0.8.1 (four doc sites corrected); src/model/record.rs comment; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "jq -r 'if .bfn == null then \"null\" else \"present\" end' entries.jsonl | sort | uniq -c; per session: for f in $(rg -lN '\"type\":\"file-history-snapshot\"' <CORPUS>/*.jsonl); do rg -N '\"type\":\"file-history-snapshot\"' $f | jq -c --arg f $f '.snapshot.trackedFileBackups|to_entries[]|{f:$f,bfn:.value.backupFileName}'; done | jq -s 'group_by(.f)|map({n:length,null_bfn:(map(select(.bfn==null))|length)})'; and on the live 2.1.258 session: rg -N '\"type\":\"file-history-snapshot\"' <CORPUS>/<SESSION>.jsonl | tail -5 | jq -c '.snapshot.trackedFileBackups|to_entries[]|{bfn:.value.backupFileName}'",
"observed": "Corpus-wide 229806 of 293298 entries carry a non-null backupFileName (78.4 percent); 63492 are null (21.6 percent). Per session in one project directory, the seven sessions with at least 250 entries read 79.3, 89.6, 89.8, 98.5, 98.6, 98.7 and 98.8 percent present. Over the five newest snapshot lines of the live 2.1.258 session, 612 of 667 present (91.8 percent). The key \"backupFileName\" itself is present in 293298 of 293298 entries; only its value is ever null.",
"rule": "One probe per (snapshot line, tracked path) entry; \"present\" means the JSON value is not null, so a present-but-null key counts as absent. Per-session rates computed only over sessions with at least 250 entries, to keep small-n sessions (which range 0-100 percent on a handful of entries) out of the band.",
"note": "The direction of the correction the claim records (usually present, not frequently null) is confirmed and is unchanged at 2.1.258. Only the band moves: 79-99 percent per session rather than 83-98, and 78.4 percent when every entry in the store is pooled, because the pooled figure is dominated by a few large low-rate sessions. The clause about correcting four earlier doc sites is a repo-history statement and no instrument on this machine can check it; it is not part of this verdict."
}
]
},
{
"id": "FH-004",
"area": "freshness-file-history",
"behavior": "Claude Code also writes `type:\"file-history-delta\"` lines - one per tracked file per write - carrying `messageId`, `snapshotMessageId`, a top-level `trackingPath` naming exactly ONE path, a `backup` object `{backupFileName, version, backupTime, realParentDir?}`, and, unlike the snapshot line, a top-level `timestamp`.",
"depends": "csift models `trackingPath` and `backup` as tolerant optional fields and promotes BOTH file-history line types to the single gated leaf `harness.meta.snapshot`; a delta parsed as a snapshot would report the wrong tracked-path set, and omitting the delta arm loses roughly 1,934 corpus lines from the searchable surface.",
"code": [
{
"path": "src/model/record.rs",
"lines": "145-153",
"snippet": " /// `file-history-delta` (v0.10.0): the ONE path this delta line tracks, beside the\n /// `backup` object below. Absent everywhere else. Additive + tolerant.\n #[serde(default, rename = \"trackingPath\")]\n pub tracking_path: Option<String>,\n\n /// `file-history-delta` `backup` object: `{backupFileName, version, backupTime,\n /// realParentDir?}`. Kept as a `Value` (tiny); read by the promoted-leaf render.\n #[serde(default)]\n pub backup: Option<serde_json::Value>,"
},
{
"path": "src/model/classify_promoted.rs",
"lines": "27",
"snippet": " \"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),"
},
{
"path": "src/search/record_text.rs",
"lines": "272-276",
"snippet": " if rec.is_type(\"file-history-delta\") {\n let path = rec.tracking_path.as_deref()?;\n let backup = rec.backup.as_ref();\n let version = ver(backup.and_then(|b| b.get(\"version\")));\n let at = rec.timestamp.as_deref().unwrap_or(\"?\");"
},
{
"path": "src/search/scan.rs",
"lines": "358-362",
"snippet": " // v0.10.0 promoted lines. Quoted-value / bare-value needles per the R13 law: the\n // `type` values `\"queue-operation\"` and `\"file-history-` (a prefix covering both\n // `-snapshot` and `-delta`), and the bare `subtype` values for the three system\n // records (a value substring survives a reserialize; prose quoting the word lands\n // on a role line and is harmless - it already parses)."
}
],
"instrument": "The census has grown since the ledger was written: 4059 snapshot lines against 1971 delta lines (was 4013 and 1934). csift stats --format json is a cheaper and exact instrument for this than a corpus-wide rg, because its line-type census validates every line rather than only prefilter candidates.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md section 5.1 and the v0.10.0 ledger; CHANGELOG 0.10.0; src/model/record.rs comment; dev sessions 2026-09-01 and 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -N '\"type\":\"file-history-delta\"' snap_raw.jsonl | jq -c 'keys' | sort | uniq -c; rg -N '\"type\":\"file-history-delta\"' snap_raw.jsonl | jq -c '.backup|keys' | sort | uniq -c; csift stats --max-count 0 --format json | jq -s '[.[]|select(.kind==\"session\")] | {sessions: length, snap: (map(.line_types[\"file-history-snapshot\"]//0)|add), delta: (map(.line_types[\"file-history-delta\"]//0)|add)}'; csift search '' @<SESSION> -t harness.meta.snapshot --count-by label --max-count 0; rg -cN '\"type\":\"file-history-snapshot\"' <CORPUS>/<SESSION>.jsonl; rg -cN '\"type\":\"file-history-delta\"' <CORPUS>/<SESSION>.jsonl",
"observed": "1539 of 1539 delta lines carry the key set [\"backup\",\"messageId\",\"snapshotMessageId\",\"timestamp\",\"trackingPath\",\"type\"] and nothing else, so the top-level timestamp the snapshot line lacks is present on every delta line. The nested backup object is [\"backupFileName\",\"backupTime\",\"realParentDir\",\"version\"] on 1037 and [\"backupFileName\",\"backupTime\",\"version\"] on 502. trackingPath is one path per line: 417 absolute, 1122 relative. Corpus line-type census over 7586 transcripts: 4059 file-history-snapshot lines in 62 sessions, 1971 file-history-delta lines in 19 sessions. csift folds both into one leaf exactly: on one session --count-by label reported 235 harness.meta.snapshot records against rg counts of 93 snapshot plus 142 delta lines.",
"rule": "One key set per raw jsonl line. The corpus counts come from csift stats' whole-file line-type census (which fully validates every line), one count per raw line, summed over every session row; the csift-versus-rg agreement check is one record per matching line in a single transcript.",
"note": "Holds at 2.1.258, and the render confirms the consumer-visible difference between the two line types: csift prints \"[file-history snapshot at <snapshot.timestamp>: ...]\" for one and \"[file-history delta at <top-level timestamp>: <trackingPath>@v<N> backup=<name>]\" for the other. One code-site caveat: the pinned classify_promoted.rs snippet matches the committed file at 13-25 byte for byte, but the working tree carries uncommitted 0.10.1 work that adds a compact_boundary arm and a MetaSystem catch-all, moving the file-history arm to line 27. The correction above anchors on the arm itself, which is stable under that edit."
}
]
},
{
"id": "FH-005",
"area": "freshness-file-history",
"behavior": "The hint is emitted only when the Bash command matches a 19-alternative FORMATTER-IDIOM regex (--write, --fix, --in-place, --auto-correct, run format, run fix, (yarn|pnpm) format, lint:file, lint:fix, black, isort, ruff format, cargo (fmt|fix), rustfmt, go fmt, terraform fmt, dprint fmt, swiftformat, phpcbf), not after every write-shaped command; a command that fails that regex is never stat-checked at all, and a backgrounded Bash call is skipped outright. When it does fire, Claude Code stats every read-set entry and keeps those whose mtime moved past both the command start and the recorded read instant. The sentence uses a real pluralizer, so the literal is \"N file\" or \"N files\", never the literal \"file(s)\"; the named list caps at 5 comma-space separated paths with \" and M more\" for the rest; each path is rendered relative to the cwd with a fallback to the raw (absolute) path when relativization fails; and the closing clause interpolates the Read tool's registered name, so the tail is \"Call <tool name> before editing.]\" rather than a fixed string.",
"depends": "`recover` parses the hint into named paths plus the remainder count, resolves each against the CARRYING record's own `cwd`, and raises a hard `hint_modified` boundary - the one signal that attributes a formatter-class rewrite to concrete files. A target hidden in the truncated remainder is undetectable: a documented limit rather than a silent gap, and a change to the sentence head or the relative-path convention would remove csift's only authoritative formatter attribution.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "22-33",
"snippet": " // ── (0) Bash result hint: `staleReadFileStateHint` names files THIS command\n // modified out of the read set. CC computed the list from its own readFileState\n // (an mtime stat over every read file), so a named match is authoritative. The\n // paths are rendered relative to the shell's cwd; resolve before matching. A\n // truncated tail (\"… and N more\") names no paths, so only the named first-5 can\n // ever match: a target hidden in the remainder is NOT detected here (documented\n // limit; the window's bash disclosure still covers the command itself).\n if let Some(hint) = tur\n .get(\"staleReadFileStateHint\")\n .and_then(serde_json::Value::as_str)\n {\n if let Some((paths, _more)) = parse_stale_read_hint(hint) {"
},
{
"path": "src/recover/carriers.rs",
"lines": "299-310",
"snippet": "/// Parse a `toolUseResult.staleReadFileStateHint` string into its named paths plus\n/// the truncated-remainder count. Wire shape (Claude Code 2.1.237, corpus-verified):\n/// `[This command modified N file(s) you've previously read: p1, p2 and M more. Call\n/// Read before editing.]` - the list caps at five names; `M` counts the unnamed rest\n/// (0 when the list is complete). Paths are comma-space separated and rendered\n/// relative to the recording shell's cwd.\npub(crate) fn parse_stale_read_hint(hint: &str) -> Option<(Vec<String>, usize)> {\n let rest = hint.strip_prefix(\"[This command modified \")?;\n let colon = rest.find(\": \")?;\n let mut tail = &rest[colon + 2..];\n if let Some(end) = tail.rfind(\". Call Read before editing.]\") {\n tail = &tail[..end];"
},
{
"path": "src/recover/types.rs",
"lines": "55-61",
"snippet": " /// Claude Code's own modified-file attribution: a Bash result's\n /// `staleReadFileStateHint` named this file as modified by the command (\"[This\n /// command modified N file(s) you've previously read: …]\"). The hint's paths are\n /// rendered RELATIVE to the recording shell's cwd and are resolved against the\n /// carrying record's own `cwd` before matching. AUTHORITATIVE (CC stat'd the\n /// read-set itself), content-less: a hard boundary at replay.\n StaleReadHint { path: String },"
}
],
"instrument": "`rg -c 'This command modified' ~/.claude/projects --glob '*.jsonl'` (577 attributions measured), then dump one value and confirm the exact prefix, the five-name cap and the ` and N more` tail; `csift recover <target> --file <a named path> --coverage --format json | jq 'select(.cause==\"hint_modified\")'` must show the boundary. Counting rule: one hint per Bash result object carrying the key, at most five named paths per hint plus a separately counted remainder.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md section 4.9 as consumed by section 6.7; AGENTS.md section 3.11; CHANGELOG 0.8.0; src/recover/carriers.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,420}This command modified.{0,200}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'function Wqo\\(.{0,300}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'jqo=new RegExp\\(\\[[^\\]]{0,900}\\]'; rg -NI 'staleReadFileStateHint' <CORPUS>/*.jsonl | jq -r '.toolUseResult.staleReadFileStateHint' > hints.txt; sed -E 's/^\\[This command modified [0-9]+ files? you.ve previously read: //; s/\\. Call Read before editing\\.\\]$//' hints.txt | awk -F', ' '{print NF}' | sort -n | uniq -c; csift recover @<SESSION> --file <REPO>/src/search.rs --coverage --format json | jq -c 'select(.kind==\"coverage\")|{events, causes:[.boundaries[].cause]}'",
"observed": "Binary, verbatim: \"let Ht=ne(),bn=5,Rn=Cn.slice(0,5).map((Tr)=>Rqo(Ht,Tr)||Tr).join(\\\", \\\"),Dn=Cn.length>5?` and ${Cn.length-5} more`:\\\"\\\",nr=t3t(dt,n.options.tools);An=`[This command modified ${Cn.length} ${H(Cn.length,\\\"file\\\")} you've previously read: ${Rn}${Dn}. Call ${nr} before editing.]`\" and the producer \"function Wqo(e,n,r){if(!jqo.test(e))return[];let o=[];return await Promise.all(Array.from(n.entries(),([d,f])=>Bx(d).then((_)=>{if(_>r&&_>f.timestamp)o.push(d)}).catch(()=>{}))),o}\", gated by jqo=new RegExp([\"--write\",\"--fix\",\"--in-place\",\"--auto-correct\",\"\\\\brun\\\\s+format\\\\b\",\"\\\\brun\\\\s+fix\\\\b\",\"\\\\b(yarn|pnpm)\\\\s+format\\\\b\",\"\\\\blint:file\\\\b\",\"\\\\blint:fix\\\\b\",\"\\\\bblack\\\\b\",\"\\\\bisort\\\\b\",\"\\\\bruff\\\\s+format\\\\b\",\"\\\\bcargo\\\\s+(fmt|fix)\\\\b\",\"\\\\brustfmt\\\\b\",\"\\\\bgo\\\\s+fmt\\\\b\",\"\\\\bterraform\\\\s+fmt\\\\b\",\"\\\\bdprint\\\\s+fmt\\\\b\",\"\\\\bswiftformat\\\\b\",\"\\\\bphpcbf\\\\b\"]). Corpus: 145 hints in one project directory over 6 transcripts; named-path histogram 73 with 1, 20 with 2, 11 with 3, 3 with 4, 3 with 5, never more; both hints carrying a remainder tail named exactly 5 and read \" and 1 more\" with a stated total of 6. Sample value: \"[This command modified 1 file you've previously read: tests/cli_integration.rs. Call Read before editing.]\". csift recover reported cause \"hint_modified\", confidence \"authoritative\", events.stale_hint 2.",
"rule": "One hint per Bash tool_result object carrying the key; named paths counted by splitting the colon-tail on \", \" after stripping the fixed head and the fixed tail, so a remainder tail glues onto the fifth name and still yields five fields.",
"note": "The cap, the remainder tail, the comma-space join and the cwd-relative rendering all hold exactly as claimed, and csift's hint_modified boundary fires on real data. Two refinements matter for csift. First, the gate is narrower than \"write-shaped\": every hint-producing command sampled from the live 2.1.258 session began with a formatter invocation, and a shell rewrite that does not match the regex produces no hint at all, which strengthens rather than weakens the documented limit. Second, parse_stale_read_hint strips a hardcoded \". Call Read before editing.]\" suffix while the binary interpolates the tool name there; the gate regex is byte-identical in 2.1.241 and 2.1.251, so this is a long-standing imprecision in the claim rather than drift. All three pinned code sites (src/recover/carriers.rs 22-33 and 299-310, src/recover/types.rs 55-61) match verbatim at the stated lines."
}
]
},
{
"id": "FH-006",
"area": "freshness-file-history",
"behavior": "A SUCCESSFUL Edit whose target had drifted on disk since the last Read, but whose old_string stayed unique so the edit applied anyway, carries toolUseResult.staleRecovered:true. Claude Code computes it as (the edit still applies) AND (a guard predicate), and reports it to its own telemetry as the \"recovered\" field of tengu_edit_tool_stale_read. The relative frequency of these successes against hard rejections is corpus-specific and does not generalize: measured 13 successes to 14 rejections across the two largest project directories in this store (13 to 8 in one, 0 to 6 in the other), against the 75 to 38 recorded earlier.",
"depends": "csift records a NON-invalidating `StaleRecovered` annotation on that window: the edited span stays trusted while the disk is disclosed as carrying unseen changes, so `recover` never presents a reconstruction as complete when it provably is not.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "131-138",
"snippet": " // `staleRecovered:true` on a SUCCESSFUL Edit: CC found the file modified on\n // disk since the last read, but old_string stayed unique so the edit applied.\n // The disk holds changes this stream never saw - an authoritative annotation.\n if tur\n .get(\"staleRecovered\")\n .and_then(serde_json::Value::as_bool)\n .unwrap_or(false)\n {"
},
{
"path": "src/recover/types.rs",
"lines": "62-67",
"snippet": " /// A SUCCESSFUL Edit whose `toolUseResult.staleRecovered:true` reports the file\n /// had been modified on disk since the last read; the edit still applied cleanly\n /// (old_string stayed unique). The buffer's edited span is right, but the disk\n /// holds other changes this stream never saw: an authoritative, NON-invalidating\n /// annotation boundary.\n StaleRecovered,"
}
],
"instrument": "`rg -c '\"staleRecovered\":true' ~/.claude/projects --glob '*.jsonl'` against `rg -c 'File has been modified since read'` over the same scope (expect roughly 2:1 successes to rejections), confirming each carrier is a non-error Edit result; then `csift recover <target> --file <path> --coverage --format json | jq '.events'` counts it under `stale_recovered`. Counting rule: one per tool_result object carrying the flag.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md sections 4.9 and 6.7; AGENTS.md section 3.11; CHANGELOG 0.8.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,260}File has been modified since read.{0,120}'; rg -N '\"staleRecovered\":true' <CORPUS>/<SESSION>.jsonl | jq -c '{type, is_err:([.message.content[]?|select(.type==\"tool_result\")|.is_error]|first), tur_keys:(.toolUseResult|keys)}'; rg -cNI '\"staleRecovered\":true' <CORPUS>/*.jsonl; rg -NI 'has been modified since read' <CORPUS>/*.jsonl | jq -r 'select(.type==\"user\")|.message.content[]?|select(.type==\"tool_result\")|select((.content|tostring)|test(\"has been modified since read\"))|(.is_error // false)|tostring' | sort | uniq -c; csift recover @<SESSION> --no-subagents --file <REPO>/tests/cli_integration.rs --coverage --format json | jq -c 'select(.kind==\"coverage\")|.events'",
"observed": "Binary Edit gate, verbatim: \"if(U){if(cB(_)>U.timestamp)if(Soe(U)&&z$(U,B));else{let ke=OQe(B,d,o),we=ke===\\\"applies\\\"&&CJ(zt,_,n,pe(n));if(s(\\\"tengu_edit_tool_stale_read\\\",{wouldHaveResult:CJt(ke),recovered:we}),!we)return{result:!1,behavior:\\\"ask\\\",message:\\\"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.\\\",errorCode:7}}}\". Every carrier sampled is a type:\"user\" record whose tool_result has no is_error and whose toolUseResult keys are [filePath,newString,oldString,originalFile,replaceAll,staleRecovered,structuredPatch,userModified]. Counts: 13 staleRecovered:true lines in the largest project directory against 8 genuine rejections there, and 0 against 6 in the second-largest, so 13 successes to 14 rejections across the two. csift recover reported events.stale_recovered 4 on a file with 4 such carriers.",
"rule": "One success per tool_result object carrying toolUseResult.staleRecovered == true. One rejection per tool_result block whose content matches \"has been modified since read\" AND whose is_error is true; the is_error filter is load-bearing, because in these two directories 45 further blocks carry the same string only as echoed file content or command output and would inflate a bare line count more than threefold.",
"note": "The mechanism and the on-disk shape hold exactly, and the binary now supplies the missing half of the story: the flag is set when the edit-applies check succeeds on a drifted file, which is precisely the condition the claim describes. Only the ratio needs a scope caveat; it inverts between the two directories measured here, so it should not be quoted as a property of the signal. Both pinned code sites (src/recover/carriers.rs 131-138, src/recover/types.rs 62-67) match verbatim at the stated lines."
}
]
},
{
"id": "FH-007",
"area": "freshness-file-history",
"behavior": "The attachment is built by walking the whole read set, skipping any entry recorded with an offset or limit (full-view reads only), requiring the file's mtime to have moved past the recorded read instant, requiring the content hash to differ, and requiring a non-empty rendered diff. It names the file in `filename` and carries the excerpt in `snippet`; `content` is the sibling edited_image_file payload's key, not an alternative spelling on the text form, and `filePath` does not appear at all in current data (0 of 1047 records). The 16 KB budget is the literal constant 16384 and is CUMULATIVE across one batch of attachments: attachments are filled in order until the running total of snippet lengths reaches 16384, after which every remaining snippet is replaced with the empty string. So an empty snippet is not a per-file overflow but a position-in-batch effect, and it is the degraded form of the same record rather than an empty edit.",
"depends": "`recover` reads it as an `ExternalEdit` HARD boundary and names the empty-snippet degraded form explicitly; reading an empty snippet as \"no change\" would silently continue a replay across an unseen external edit, and a payload key rename would drop the only content-bearing external-edit signal. The attachment cannot distinguish a human edit from a formatter run, which csift discloses.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "218-230",
"snippet": " // (7a) edited_text_file → an external edit (hard boundary).\n if atype == Some(\"edited_text_file\") {\n let path = att\n .get(\"filename\")\n .or_else(|| att.get(\"filePath\"))\n .and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {\n let snippet_text = att\n .get(\"snippet\")\n .or_else(|| att.get(\"content\"))\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default();\n let snippet = strip_gutter(snippet_text);"
},
{
"path": "src/recover/types.rs",
"lines": "50-54",
"snippet": " /// An external/user edit captured as an `edited_text_file` attachment snippet.\n /// An EMPTY snippet is the attachment's over-budget degraded form (the change\n /// exceeded the 16KB budget): the signal is still authoritative, the content is\n /// not carried.\n ExternalEdit { snippet: Vec<(usize, String)> },"
},
{
"path": "src/recover/replay.rs",
"lines": "207-213",
"snippet": " // An EMPTY snippet is the attachment's over-budget degraded form: the\n // change exceeded the 16KB budget, so the signal arrives content-less.\n let detail = if snippet.is_empty() {\n \"edited_text_file attachment (file changed outside the tool stream; \\\n no snippet: the change exceeded the attachment budget)\"\n } else {\n \"edited_text_file attachment (file changed outside the tool stream)\""
}
],
"instrument": "`rg -c 'edited_text_file' ~/.claude/projects --glob '*.jsonl'` (1,898 occurrences across 133 files measured; a separate per-record census counted 1,322-1,328 attachment records of that payload type), then `jq -r 'select(.attachment.type==\"edited_text_file\") | [(.attachment.filename // .attachment.filePath), ((.attachment.snippet // \"\")|length)] | @tsv' <file>` to see names and snippet sizes including zeros. Counting rule: one attachment record per line whose attachment payload `type` is `edited_text_file`.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md sections 4.9 and 6.7; AGENTS.md section 3.11; CHANGELOG 0.8.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,700}\"edited_text_file\".{0,300}'; csift search '' --count-by attachment --max-count 0; csift search '\"type\":\"edited_text_file\"' --attachments -t harness.meta.attachment --raw --max-count 0 > etf_raw.jsonl; jq -c 'select(.type==\"attachment\")|.attachment|keys' etf_raw.jsonl | sort | uniq -c; jq -r 'select(.type==\"attachment\")|.attachment|if ((.snippet // .content // \"\")|length)==0 then \"empty\" else \"nonempty\" end' etf_raw.jsonl | sort | uniq -c; csift recover @<SESSION> --no-subagents --file <REPO>/Cargo.toml --coverage --format json | jq -c 'select(.kind==\"coverage\")|{events, causes:[.boundaries[].cause]}'",
"observed": "Binary, verbatim: \"var vQo=16384\" and the builder \"async function QSr(e){let n=K1e(e.readFileState); ... let v=e.readFileState.get(_);if(!v)return null;if(v.offset!==void 0||v.limit!==void 0)return null; ... if(await Bx(C)<=v.timestamp)return null; ... if(U.data.file.truncatedByTokenCap===!0)return null;if(z$(v,U.data.file.content))return null;let j=EJt(v.content,U.data.file.content);if(j===\\\"\\\")return null;return{type:\\\"edited_text_file\\\",filename:C,snippet:j}\" followed by \"let f=0;for(let _ of d){if(_.type!==\\\"edited_text_file\\\")continue;if(f>=vQo)_.snippet=\\\"\\\";else f+=_.snippet.length}\". Corpus: csift's attachment census reports 1334 edited_text_file records across all projects; of 1047 raw lines retrieved, 1047 carry the key set [\"filename\",\"snippet\",\"type\"] and zero carry filePath or content; 61 have a zero-length snippet and 986 do not. Snippet byte lengths over those 1047: min 0, median 7925, p90 8212, max 8223. csift recover on one target reported causes external_edit x3 with detail \"edited_text_file attachment (file changed outside the tool stream); this can be an external edit, the project's formatter, or a hook\".",
"rule": "One attachment record per line whose attachment payload type is edited_text_file; key sets unioned per record; a snippet counts as empty when its JSON string length is zero.",
"note": "The claim's load-bearing assertions all hold and the binary makes them exact: read-set gating, full-view-read gating, the 16384 budget, and the empty-snippet degraded form are all directly readable in one function. Three refinements: the budget is cumulative per batch rather than per file (which explains 61 empties among 1047 records while no single snippet exceeds 8223 bytes, the diff renderer's own ceiling); the filePath and content fallbacks in csift are unobserved against current CC and cost nothing; and the \"one shot per read epoch\" clause is the one part no instrument here decided, since it would need a controlled sequence of writes without an intervening Read. All three pinned code sites (src/recover/carriers.rs 218-230, src/recover/types.rs 50-54, src/recover/replay.rs 207-213) match verbatim at the stated lines."
}
]
},
{
"id": "FH-008",
"area": "freshness-file-history",
"behavior": "The edited_text_file snippet carries a leading line-number gutter whose separator is a TAB (U+0009) in Claude Code 2.1.258, on 986 of 986 non-empty payloads measured. The U+2192 arrow form is not produced by 2.1.258 or by 2.1.241 (the character does not occur in either binary's strings) and does not occur anywhere in this store, so the \"older form\" is not observable here.",
"depends": "csift strips BOTH gutter forms and skips any line with no recognizable gutter rather than fabricating a number; an arrow-only parser would drop every current-format snippet.",
"code": [
{
"path": "src/recover/diff.rs",
"lines": "185-189",
"snippet": "/// Strip a leading line-number gutter from each line of a cat -n style snippet. Handles\n/// BOTH the TAB gutter (`\\d+\\t<text>`, what current CC Read content uses) and the arrow\n/// gutter (`\\d+→<text>`, an older form). Returns `(file_line_no, text)` pairs; a line\n/// with no recognizable gutter is skipped (we never fabricate a number).\npub(crate) fn strip_gutter(snippet: &str) -> Vec<(usize, String)> {"
}
],
"instrument": "Inspect one payload: `jq -r 'select(.attachment.type==\"edited_text_file\") | .attachment.snippet' <file> | head -3 | cat -A` and look at the byte immediately after the leading digits. Counting rule: one probe per attachment payload.",
"located": {
"claude_code": "2.1.258",
"csift": "0.8.0",
"source": "SPEC.md section 6.7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "jq -r 'select(.type==\"attachment\")|.attachment.snippet|select(length>0)|split(\"\\n\")[0]' etf_raw.jsonl | perl -ne 'if(/^(\\d+)(.)/){printf(\"U+%04X\\n\", ord($2))}else{print \"NO-GUTTER\\n\"}' | sort | uniq -c; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c $'\\u2192'; strings -n 6 ~/.local/share/claude/versions/2.1.241 | rg -c $'\\u2192'",
"observed": "986 of 986 non-empty snippets have U+0009 immediately after the leading digits of the first line; zero read NO-GUTTER and zero read any other codepoint. rg found no U+2192 anywhere in the strings of 2.1.258 or of 2.1.241. No arrow-gutter snippet exists anywhere in the live corpus sample of 1047 edited_text_file payloads.",
"rule": "One probe per non-empty attachment payload, reading the single byte immediately following the leading run of digits on the payload's first line.",
"note": "The half that governs correctness today is instrumented and holds: an arrow-only parser would drop every current-format snippet, and csift's TAB arm covers 986 of 986. The arrow half is unverifiable on this machine, because the oldest Claude Code build present is 2.1.241 and it already has no arrow; deciding it would need a build old enough to emit one, or a transcript from that build. Keeping the arrow arm costs nothing and is the safe choice, so the code needs no change. src/recover/diff.rs:185-189 matches verbatim."
}
]
},
{
"id": "FH-009",
"area": "freshness-file-history",
"behavior": "Claude Code's integrity error bodies at 2.1.258 are \"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.\", \"File has not been read yet. Read it first before writing to it.\", \"String to replace not found in file.\" followed by a newline and a \"String: \" line quoting the failed old_string, and \"File does not exist.\" in several forms. The first three arrive wrapped in a <tool_use_error> element and carry no path, so the file must be recovered through the tool_use_id join. The fourth does NOT: the observed form appends \"Note: your current working directory is <absolute path>.\" and is unwrapped, and the binary carries a further \"File does not exist: <filePath>\" form and a \"Did you mean ...?\" suffix. There is no separate \"Read it first\" variant: every observed not-read-yet body carries the full sentence, and the bare \"File has not been read yet\" string in the binary is a UI-side prefix test, not a tool_result body.",
"depends": "csift classifies only the modified-since-read case as a hard boundary and counts the rest as annotations, attributing each to a file through the `tool_use_id` join; a wrong classification either fabricates boundaries or lets a stale buffer through as current.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "281-297",
"snippet": "/// Classify a tool_result error body as an integrity error, or `None` if it is some\n/// other tool error. Only [`IntegrityKind::ModifiedSinceRead`] becomes a boundary;\n/// the others are COUNTED annotations (the op never landed).\npub(crate) fn classify_integrity_error(content: &serde_json::Value) -> Option<IntegrityKind> {\n let text = crate::model::tool_result_content_text(content);\n if text.contains(\"has been modified since read\") || text.contains(\"File has been modified\") {\n Some(IntegrityKind::ModifiedSinceRead)\n } else if text.contains(\"has not been read yet\") || text.contains(\"Read it first\") {\n Some(IntegrityKind::NotReadYet)\n } else if text.contains(\"String to replace not found in file\") {\n Some(IntegrityKind::StringNotFound)\n } else if text.contains(\"File does not exist\") {\n Some(IntegrityKind::FileDoesNotExist)\n } else {\n None\n }\n}"
}
],
"instrument": "`rg -c 'has been modified since read|String to replace not found in file' ~/.claude/projects --glob '*.jsonl'`, then `csift files @<session> --format json | jq 'select(.kind==\"boundary\")'`. Counting rule: one boundary per erroring tool_result carrier.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'File has been modified since read[^\"`]{0,90}|has not been read yet[^\"`]{0,80}|String to replace not found[^\"`]{0,60}|File does not exist[^\"`]{0,50}' | sort -u; for pat in 'File has been modified since read' 'File has not been read yet' 'String to replace not found in file' 'File does not exist'; do rg -NI \"$pat\" <CORPUS>/*.jsonl | jq -r --arg p \"$pat\" 'select(.type==\"user\")|.message.content[]?|select(.type==\"tool_result\")|select(((.content|tostring)|test($p)) and (.is_error==true))|(.content|tostring)[0:170]' | sort | uniq -c; done; csift recover @<SESSION> --no-subagents --file <REPO>/tests/cli_integration.rs --coverage --format json | jq -c 'select(.kind==\"coverage\")|{events, causes:[.boundaries[].cause]}'",
"observed": "All four literals exist in 2.1.258. The observed error bodies are longer than the pinned literals: \"<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>\" (8 records), \"<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>\" (44), \"<tool_use_error>String to replace not found in file.\\nString: <the failed old_string>\" (7), and \"File does not exist. Note: your current working directory is <an absolute path>.\" (2). The last carries an inline absolute path and is NOT wrapped in a tool_use_error element. The binary also holds \"File does not exist: ${e.filePath}\" and a \"Did you mean\" variant. Joining three rejections to their tool_use records recovered Edit with file_path in each case. csift recover reported causes modified_since_read x2 and events.integrity_error 2.",
"rule": "One body per erroring tool_result block, deduplicated by the first 170 characters; only blocks with is_error true are counted, which excludes the many records where the same strings appear as echoed file content.",
"note": "The classifier is safe as written: every observed body is matched by the substring test it targets, and the ordering keeps a String-not-found body (which quotes arbitrary user text) from falling through to a later arm. The clause that needs striking is \"whose text carries no inline path\", which is true of three bodies and false of the fourth; a consumer that assumed it universally would be surprised by a File-does-not-exist body carrying the shell cwd. src/recover/carriers.rs:281-297 matches verbatim."
}
]
},
{
"id": "FH-010",
"area": "freshness-file-history",
"behavior": "Write and Edit both gate on an mtime check against the last Read, with a content-hash absolution (a base-36 hash of the read content) that Soe() allows only when the recorded read had offset <= 1 and was not a partial view. A byte-identical rewrite therefore never trips the gate. The correction: a windowed read does not merely lose its absolution channel and then reject on mtime movement. Both tools branch on \"no read state OR partial view\" BEFORE reaching the mtime comparison and reject from that arm with \"File has not been read yet. Read it first before writing to it.\" (errorCode 2 on Write), so after a windowed read the write is refused whether or not the file's mtime moved, unless a separate guard-skip predicate applies. The notebook editor carries a third copy of the mtime gate with no absolution branch at all (errorCode 10).",
"depends": "csift's `recover` treats a modified-since-read rejection as a hard integrity boundary that invalidates every known line before it; if the gate's criterion changed, recover would either over-report gaps or present stale content as current.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "7-11",
"snippet": "/// Extract a `FullSnapshot` / `PartialRead` (Read) or a `FullSnapshot` (Write) / `Edit`\n/// from a `toolUseResult` carrier, plus the two Claude Code freshness signals riding\n/// the same carrier: a Bash result's `staleReadFileStateHint` (CC's own modified-file\n/// attribution) and an Edit result's `staleRecovered` flag. `record_cwd` is the\n/// carrying record's own `cwd`, the base the hint's relative paths resolve against."
},
{
"path": "src/cli/files_recover_args/recover.rs",
"lines": "129-137",
"snippet": " FRESHNESS SIGNALS (Claude Code's own, adopted as boundaries)\\n \\\n A Bash result's `staleReadFileStateHint` is Claude Code itself reporting \\\n that the command modified files in its read set, BY NAME (paths relative to \\\n the shell cwd, resolved before matching): an authoritative HARD \\\n `hint_modified` boundary, and the one signal that attributes a formatter-class \\\n rewrite to concrete files. A successful Edit flagged `staleRecovered` means \\\n the disk had drifted since the last read but the edit still applied: an \\\n authoritative `stale_recovered` annotation (nothing invalidated, other \\\n changes exist outside the stream). An `edited_text_file` external-edit \\"
}
],
"instrument": "`rg -c 'File has been modified since read' ~/.claude/projects --glob '*.jsonl'` (370 occurrences across 79 files measured) and confirm every carrier has `is_error:true`; live check: Read a file, modify it outside Claude Code, then Edit it. Counting rule: one occurrence per matching tool_result text.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md section 4.9; AGENTS.md section 3.11"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'function Soe\\(e\\)\\{.{0,320}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,260}File has been modified since read.{0,120}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,120}isPartialView.{0,120}' | sort -u; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'function k\\(e\\)\\{return Bun\\.hash.{0,60}'",
"observed": "The absolution predicate, verbatim: \"function Soe(e){if((e.offset??1)>1||e.isPartialView)return!1;if(e.limit===void 0)return!0;return e.content!==\\\"\\\"&&_n(e.content,\" ... The Write gate: \"if(Math.floor(I)>F.timestamp){let U=Soe(F),j=!1;if(U){let W=await C.readFileBytes(d);j=Rtr(F,W.toString(\\\"utf8\\\"))}if(!j)return{result:!1,message:\\\"File has been modified since read, ...\\\",errorCode:3}}\". The Edit gate: \"if(cB(_)>U.timestamp)if(Soe(U)&&z$(U,B));else{...}\". Both tools first branch on \"if(!F||F.isPartialView)\" / \"if(!U||U.isPartialView)\" and return \"File has not been read yet. Read it first before writing to it.\" from that arm. The content hash is \"function k(e){return Bun.hash(e).toString(36)}\". The literal appears 4 times in the binary: Write (errorCode 3), Edit (errorCode 7), notebook edit (errorCode 10), and the standalone message.",
"rule": "One gate per tool, read off the decompiled control flow around each occurrence of the literal error message; the predicate is read from its own named function rather than inferred from call sites.",
"note": "The criterion csift depends on is intact at 2.1.258 and is now readable as source rather than inferred: mtime comparison, full-read-only hash absolution, idempotent rewrites passing. The refinement makes the rejection stricter, not looser, than the claim states, so recover's treatment of a modified-since-read rejection as a hard boundary is unaffected; what changes is which message a windowed read produces, and a consumer expecting \"modified since read\" after a windowed read will instead see \"not been read yet\". Both pinned code sites (src/recover/carriers.rs 7-11, src/cli/files_recover_args/recover.rs 129-137) match verbatim at the stated lines."
}
]
},
{
"id": "FH-011",
"area": "freshness-file-history",
"behavior": "A read-before-write or staleness REJECTION always lands as a tool_result with `is_error:true`: the validator's `behavior:'ask'` field is inert in the runner (measured 25 of 25 rejection records in one corpus).",
"depends": "csift's recover boundary detection keys on the error flag plus the message class, so it has no blind spot from a non-errored rejection path; if rejections could land as successes the integrity boundaries would be missed entirely.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "1-3",
"snippet": "//! Carrier-side extraction: toolUseResult (Read/Write/Edit + the Bash freshness\n//! hint), attachments (edited_text_file / file), the integrity-error classifier, and\n//! the structured-patch parser."
}
],
"instrument": "Search `~/.claude/projects` for `File has been modified since read` and check the enclosing tool_result block's `is_error`; expect true on every one. Counting rule: one check per matching tool_result block.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md section 4.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -NI --glob '*.jsonl' '<tool_use_error>' <one-project-dir> | jq -r '(.message.content//[])|if type==\"array\" then . else [] end|.[]|select(.type==\"tool_result\")|(if (.content|type)==\"string\" then .content else ((.content//[])|map(select(.type==\"text\")|.text)|join(\"\\n\")) end) as $t|select($t|startswith(\"<tool_use_error>\"))|select($t|test(\"^<tool_use_error>(File has been modified since read|File has not been read yet|String to replace not found|File does not exist)\"))|[($t|capture(\"^<tool_use_error>(?<k>File has been modified since read|File has not been read yet|String to replace not found|File does not exist)\").k),((.is_error//\"ABSENT\")|tostring)]|@tsv' | sort | uniq -c AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}behavior:\"ask\".{0,200}'",
"observed": "77 rejection carriers, is_error:true on 77 of 77, zero with the key absent or false: 'File has been modified since read' 8, 'File has not been read yet' 60, 'String to replace not found' 9. Binary, the Edit validator's own return: 'if(s(\"tengu_edit_tool_stale_read\",{wouldHaveResult:CJt(ke),recovered:we}),!we)return{result:!1,behavior:\"ask\",message:\"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.\",errorCode:7}'. The Write validator's twin returns the same message with no behavior field: '{result:!1,message:\"File has been modified since read, ...\",errorCode:3}'.",
"rule": "One carrier per tool_result block whose text STARTS with '<tool_use_error>' followed by one of the four integrity messages. The startswith anchor is load-bearing: a loose substring test over the same scope returned 70 extra hits that were all quotations of the literal inside ordinary tool output (a source file read back, a grep result, an earlier test log), none of them rejections. Counting those as carriers would report 70 non-errored rejections that do not exist.",
"note": "Both halves confirmed by separate instruments. The 'behavior:\"ask\"' field is literally present in the Edit tool's staleness return in 2.1.258 (errorCode:7), and it is inert at the runner: every rejection reached the transcript as an errored tool_result, none as a permission prompt. Related observation worth carrying forward, since it bounds when the gate fires at all: the Write validator has an early bypass ('if(Eu(d))return{result:!0}') and a skip path guarded by 'let W=!F&&!yIn(d)&&!mYe(U,n.remoteCall)&&CJ(jn,d,n,pe(n))' whose telemetry event is named 'tengu_write_tool_not_read_hypothetical' with a 'guardSkipped' field, and the shared helper 'FRo' returns without throwing when there is no prior read, no pre-read guard and reads are auto-allowed. Live in this session, a Write and an Edit to files never opened with the Read tool both succeeded with no rejection, so the not-read-yet gate is not universally armed."
}
]
},
{
"id": "FH-012",
"area": "freshness-file-history",
"behavior": "Bash NEVER invalidates readFileState - that is exactly how staleness arises - but Claude Code lexically recognizes read-shaped Bash commands and SEEDS read-state from them, so a bash `cat <file>` legalizes a later Write of that file with no intervening Read.",
"depends": "`recover` and `files` treat a gated bash read idiom as a real read anchor for the same reason the harness does; assuming only the Read tool seeds state under-counts legal Writes and over-reports integrity boundaries.",
"code": [
{
"path": "src/bash_mutations/anchors.rs",
"lines": "4-14",
"snippet": "//! A small closed set of shell shapes carries DETERMINISTIC file content in the\n//! transcript itself: a quoted-delimiter heredoc's body is byte-verbatim in the\n//! tool_use input; `cat <file>` / `head -n N <file>` / `sed -n 'A,Bp' <file>` stdout\n//! (under the caller's completeness gate) IS the file window; `echo`/`printf` with\n//! purely literal arguments write known bytes; `truncate -s 0` writes the empty file.\n//! Everything else stays in the boundary/heuristic lanes - correctness first, but\n//! honesty about the decidable subset is not surrender on it.\n//!\n//! ADMISSION LAWS (each refusal falls back to today's behavior, never a wrong anchor):\n//! - A READ anchor demands a SINGLE simple segment: a compound command's stdout is a\n//! concatenation nothing can attribute to one file."
},
{
"path": "src/recover/carriers.rs",
"lines": "22-28",
"snippet": " // ── (0) Bash result hint: `staleReadFileStateHint` names files THIS command\n // modified out of the read set. CC computed the list from its own readFileState\n // (an mtime stat over every read file), so a named match is authoritative. The\n // paths are rendered relative to the shell's cwd; resolve before matching. A\n // truncated tail (\"… and N more\") names no paths, so only the named first-5 can\n // ever match: a target hidden in the remainder is NOT detected here (documented\n // limit; the window's bash disclosure still covers the command itself)."
}
],
"instrument": "Live: `cat <file>` through the Bash tool, then Write to it - the Write must succeed with no freshness rejection. Corpus: find a Write immediately following a bash `cat` of the same path with no Read in between and no rejection. Counting rule: one tool_result per attempt, one witness sequence per session.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md section 4.9; AGENTS.md section 3.11"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Jzo\\(.{0,1500}' AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '(async )?function hWn\\(.{0,900}' AND a live A/B: Bash 'cat <scratch>/probe.txt' as a lone command, then a later out-of-band Bash 'printf ... > <scratch>/probe.txt', versus two control files rewritten identically that were never cat-ed by a lone command.",
"observed": "Binary, the read-shape recognizer: 'function Jzo(e){if(/[|<>]/.test(e))return[];...}' dispatching to a sed -n 'A,Bp' arm and a table 'eqo=new Map([[\"cat\",new Set([\"-n\",\"--number\"])],[\"nl\",new Set],[\"bat\",...],[\"batcat\",...]])'. Binary, the seeder run after every non-interrupted foreground Bash call: 'async function hWn(e,n,r,o,d){let f=Jzo(e).filter((v)=>!v.requiresExitZero||o===0);if(f.length===0)return;...let C=ct(v.filePath);if(n.get(C))return;...if(I.size>10485760)return;...n.set(C,{content:U.content,timestamp:Math.floor(I.mtimeMs),offset:U.offset,limit:U.limit,...})}'. Live: after the lone cat and the out-of-band rewrite, the harness returned 'Note: <path> changed on disk since you last read it.' followed by the file's numbered content, for a file never opened with the Read tool. The two control files, rewritten by the same idiom but never cat-ed by a lone command, produced no note.",
"rule": "One seeding probe per (bash command, file). A control must be a lone command: the recognizer refuses the whole command string when it contains '|', '<' or '>', so a cat that shares a line with a redirect seeds nothing. The first attempt at this A/B was void for exactly that reason - the cat rode a compound command carrying '>' - and had to be rerun with the cat isolated.",
"note": "Both clauses hold. Seeding: a lone cat / nl / bat / batcat, or a sed -n 'A,Bp', enters readFileState with the file's content, its mtime as the read timestamp, and (for sed) an offset/limit partial view. Non-invalidation: the seeder's 'if(n.get(C))return' means bash never overwrites an existing entry, and the write side only computes a hint, never deletes state - the one readFileState.delete site found is 'evictRewoundFileTracking', the rewind feature. The 10485760-byte (10 MiB) size cap on seeding is a bound the claim does not mention."
}
]
},
{
"id": "FH-013",
"area": "freshness-file-history",
"behavior": "A file change that produces none of the three freshness signals - no `staleReadFileStateHint`, no `staleRecovered`, no `edited_text_file` attachment - leaves NO transcript trace at all, so absence of a signal is never evidence of absence of a change.",
"depends": "csift cannot infer a modification from a bash write alone, so every recovered window carries explicit opaque accounting (`boundaries`, `bash_file`, `bash_opaque`) plus a suggested search instead of a silent gap, and a `complete` row with non-zero columns means complete FROM THE TOOL STREAM, not verified against disk.",
"code": [
{
"path": "src/recover/scan.rs",
"lines": "138-143",
"snippet": "/// Write `recovery-report.tsv` under `out_dir` and print the one-line summary. The\n/// three accounting columns disclose what each recovered file's window held beyond\n/// the replay: `boundaries` (integrity boundaries), `bash_file` (parsed bash\n/// mutations of the file), `bash_opaque` (mutating-class + PowerShell commands whose\n/// file set is unknowable). A `complete` row with non-zero columns is complete FROM\n/// THE TOOL STREAM, not verified against disk."
},
{
"path": "src/recover/scan.rs",
"lines": "321-327",
"snippet": "/// Collect the SCOPE-ACCOUNTING commands of one transcript: every mutating-CLASS\n/// marker a Bash command yields (`fmt:cargo`, `interp:python`, `pkg:npm`,\n/// `extract:tar`, `git:<sub>` - commands that mutate files they never name), plus\n/// every `PowerShell` tool call (its command text is never lexically parsed, so any\n/// file it touched is invisible; see AGENTS 3.9). Target-independent by nature: these\n/// commands CANNOT be joined to a `--file`, which is exactly why they are counted and\n/// disclosed per window instead of silently ignored."
}
],
"instrument": "`csift recover <target> --file <path> --coverage --format json | jq '{events, boundaries}'` on a session where a whole-tree formatter ran: windows with no signal are reported opaque rather than clean. Counting rule: one opaque window per turn that produced no freshness signal.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "SPEC.md section 4.9; AGENTS.md section 3.11; CHANGELOG 0.8.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Live: create and then rewrite three scratch files with Bash 'printf ... > <path>', then census the session transcript for the three signals - rg -N '<probe name>' <transcript> | jq -r 'select(.toolUseResult.staleReadFileStateHint != null)|1' | wc -l, and the same for .toolUseResult.staleRecovered==true and .attachment.type==\"edited_text_file\". csift side: csift recover @<session> --no-subagents --file <repo>/src/search/matcher.rs --coverage",
"observed": "All three probe files: staleReadFileStateHint 0, staleRecovered 0, edited_text_file 0 - no transcript record of any kind carried a freshness signal for them, including the one file whose change the harness had visibly noticed in the tool result. csift recover printed the accounting rather than a clean window: 'events: 5 read (1 full, 4 windowed) - 2 edit - 2 modified-file-hint', 'integrity boundaries: 2 (2 hard - 0 soft)', 'opaque in window: 490 mutating-class command(s) whose file set is not in the command text (extract:tar x24, fmt:cargo x195, git:checkout x5, git:mv x3, git:reset x4, interp:python x258, pkg:cargo)', and the suggested search line.",
"rule": "One probe per (file, bash rewrite). A signal counts only when a transcript record carries the key - the model-facing note in a tool result is not a record and does not count. One opaque row per mutating-class command in the window, as csift counts them.",
"note": "Stronger than the claim states. Even the change the harness DID detect and report to the model in-band left no durable freshness signal anywhere in the transcript, so the absence is not merely 'no signal for unnoticed changes' but 'no signal for a bash write, noticed or not'. csift's disclosure side behaves as described: the window reports 490 unattributable commands and two authoritative hint boundaries instead of a silent gap."
}
]
},
{
"id": "FH-014",
"area": "freshness-file-history",
"behavior": "Claude Code writes a `file-history-snapshot` at the PROMPT-SUBMISSION boundary rather than on any turn-segmentation rule, and slash commands and meta prompts fire one too, which is why an in-process `/model` write is caught at all. The snapshot count therefore tracks prompts, not csift turns, and may fall either side of the turn count: three older transcripts measured 91/76, 57/53 and 28/4 snapshots-to-turns, while a current-Claude-Code window measured 23 snapshots against 27 turns and 20 prompt submissions. Since about 2.1.227 a newly tracked file's first backup is carried by a separate `file-history-delta` line instead of waiting for the next snapshot, so counting only snapshot lines undercounts file-history events - the same transcript held 95 snapshots and 146 deltas.",
"depends": "csift attributes an external write to the INTERVAL between two snapshot lines rather than to an instant, and its `files` timeline row names the version transition plus that interval; assuming a per-turn cadence would mis-place every attribution.",
"code": [
{
"path": "src/files/external.rs",
"lines": "34-37",
"snippet": "/// Synthesize `external write` timeline rows for the settings family from the\n/// snapshot version sequences. `mutations` are this transcript's tool-extracted\n/// rows (line-ordered per path is NOT required; membership in the interval is by\n/// jsonl line number)."
}
],
"instrument": "`csift stats @<session>` for the turn count, then count `\"type\":\"file-history-snapshot\"` lines in the same transcript; expect more snapshots than turns. Counting rule: one snapshot per matching line, one turn per csift turn index.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Per transcript: count lines with type \"file-history-snapshot\" and lines with type \"file-history-delta\" in jsonl order (python3, json.loads on matching lines only), against csift stats @<session> --no-subagents --format json | jq -r 'select(.kind==\"session\")|.turns'. Window form: csift stats @<session> --no-subagents --since 2026-09-02 and csift search '' @<session> --no-subagents --since 2026-09-02 --count-by label. Adjacency form: for each snapshot line, walk back to the nearest record that is not itself a file-history line and record its type.",
"observed": "Older transcripts: 91 snapshots vs 76 turns; 57 vs 53; 28 vs 4 - snapshots exceed turns. The live transcript, which spans Claude Code 2.1.227 through 2.1.258: 95 snapshot lines vs 192 turns, alongside 146 file-history-delta lines (241 file-history lines in total). Its 2026-09-02 window (2.1.257/2.1.258): 23 snapshots, 27 csift turns, 18 user.message plus 2 harness.command.invocation records. Predecessor of each of those 23 snapshots: attachment 19, user 3, session-bridge record 1.",
"rule": "One snapshot per raw jsonl line of that type; one turn per csift turn index at csift 0.10.0 with --no-subagents; one prompt submission per user.message or harness.command.invocation record in the same window; one adjacency row per snapshot line.",
"note": "The mechanism survives and the adjacency evidence is direct: 22 of 23 snapshots in the current-Claude-Code window sit immediately after the prompt's own attachment or user record. What does not survive is 'more snapshots than turns' as a signature - it inverted in the current window. The claim's own example transcript still carries exactly 91 snapshots, but its csift turn count reads 76 under csift 0.10.0, not the 55 recorded; anything keyed to that ratio should be rechecked against the version of csift that will read it."
}
]
},
{
"id": "FH-015",
"area": "freshness-file-history",
"behavior": "Claude Code snapshots every Edit/Write-tracked file per prompt and bumps `trackedFileBackups[<path>].version` ONLY when the file's bytes changed - one measured path sat at v2 across 88 consecutive snapshots - so a version JUMP with no tool write of that path in the interval is an external write.",
"depends": "`files --by timeline` synthesizes its `external write` row from exactly that instrument and `recover` rebases its replay at a divergent snapshot; without it a replayed file reports a state that never existed on disk.",
"code": [
{
"path": "src/files/external.rs",
"lines": "4-8",
"snippet": "//! Claude Code rewrites its settings files IN-PROCESS (/model, /config, theme,\n//! permission \"always allow\", plugin toggles) - no tool record, usually no printed\n//! trace. The instrument: CC snapshots every Edit/Write-tracked file per prompt and\n//! bumps `trackedFileBackups[<path>].version` only when the bytes changed; a version\n//! JUMP with no tool write of that path in the interval is an external write."
},
{
"path": "src/recover/replay.rs",
"lines": "409-414",
"snippet": " out.counts.history_snapshot += 1;\n // The file-history INSTRUMENT (v0.9.4): Claude Code backs the tracked\n // file up per prompt and bumps `version` only when the bytes changed.\n // A version CHANGE therefore captures the disk truth at that instant -\n // including harness-side writes that leave NO tool record (measured:\n // half of all settings.json mutations corpus-wide). Three cases:"
}
],
"instrument": "Walk `.snapshot.trackedFileBackups | to_entries[] | [.key, .value.version]` in line order over one transcript: long constant runs broken by single increments; then `csift files @<session> --format json | jq 'select(.op==\"external_write\")'`. Counting rule: one version transition per (snapshot line, tracked path) pair.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger; CHANGELOG 0.9.4; src/files/external.rs comment; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function bX\\(e,n,r,o\\).{0,2100}' AND the corpus run census: per (transcript, tracked path), walk trackedFileBackups[<path>].version in jsonl line order and record the length of each constant run and the number of changes.",
"observed": "Binary change test: 'function I3r(e,n,r){if(e===null!==(n===null))return!0;if(e===null||n===null)return!1;if(!e.isFile()||!n.isFile())return!0;if(e.mode!==n.mode||e.size!==n.size)return!0;if(e.mtimeMs<n.mtimeMs)return!1;return r()}', where r() is 'let[_,v]=await Promise.all([M8(e),M8(o,{noFollow:!0})]);if(_===null||v===null)return!0;return _!==v' - a content comparison of the file against its stored backup. In the snapshot builder, when that test is false the previous entry is reused unchanged ('ave(f,v,I.realParentDir!==void 0&&W===I.version?I:{...I,version:W,...});return'), and only otherwise is a new backup written at 'U=B+1' via 'ave(f,v,await XQt(C,U))'. Corpus: 4,061 snapshot lines across 62 transcripts carrying snapshots, 56,940 version changes, longest constant run 1,241 snapshots at one version.",
"rule": "One entry per (snapshot line, tracked path) pair in jsonl line order; a run is a maximal stretch of adjacent pairs with an equal version; a change is one adjacent pair with unequal versions.",
"note": "Confirmed at the mechanism level, not just correlationally: the bump is gated on mode, size and then a content comparison against the stored backup, so a byte-identical rewrite with a bumped mtime provably does not bump the version. The claim's '88 consecutive snapshots at one version' is a floor - the corpus maximum is 1,241."
}
]
},
{
"id": "FH-016",
"area": "freshness-file-history",
"behavior": "The `version` counter RESETS mid-session, so the sequence is a series of GENERATIONS rather than one monotone counter and a decrease is a new generation, never an anomaly: a corpus census measured 159 decreases across the 62 transcripts that carry snapshot lines, concentrated in 4 of them and absent from the other 58.",
"depends": "csift reads version JUMPS only within a generation and treats a decrease as bookkeeping; a naive `version > prev` test would silently drop every post-reset event, and cross-generation comparison is invalid by construction.",
"code": [
{
"path": "src/files/external.rs",
"lines": "16-21",
"snippet": "//! HONEST LIMITS (documented, disclosed in SPEC/SKILL): the version counter RESETS\n//! mid-session (a process restart starts a new generation) - jumps are only read\n//! within a generation, so a write hiding across a reset is not reported; a tool\n//! write and a silent write in the SAME snapshot interval merge into one bump and\n//! the silent half is invisible here (recover's content comparison is the complete\n//! form); a session that never tool-touched the file has no tracking at all."
},
{
"path": "src/recover/types.rs",
"lines": "80-88",
"snippet": " /// A `file-history-snapshot` recorded a disk backup of `--file` at this time.\n /// Since v0.9.4 the marker carries its INSTRUMENT payload: the per-path\n /// `version` (monotone within a GENERATION; the counter resets mid-session on\n /// process restart - 148 real occurrences - so a DECREASE means a new\n /// generation, never an anomaly), the store `backupFileName`, its `backupTime`,\n /// and - attached by the scan post-pass ONLY on a version CHANGE, and only when\n /// the store file exists AND its mtime agrees with `backupTime` (the @vN name\n /// COLLIDES across a generation reset, so an unverified read can return the\n /// wrong generation's bytes) - the snapshot CONTENT. The replay layer uses a"
},
{
"path": "src/recover/snapshots.rs",
"lines": "10-14",
"snippet": "//! and its mtime must agree with the marker's `backupTime` within a tolerance -\n//! the `@vN` file name COLLIDES across a mid-session generation reset (the version\n//! counter restarts; measured 148 resets), so an unverified read can silently\n//! return the WRONG generation's bytes. Non-UTF-8 store bytes are refused (the\n//! replay buffer is line-textual)."
}
],
"instrument": "Per (transcript, tracked path), walk the `trackedFileBackups[<path>].version` sequence in jsonl line order and count strict decreases. Counting rule: one reset per adjacent pair where `version[i] < version[i-1]`.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger; src/recover/snapshots.rs comment; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Corpus census over every project directory under ~/.claude/projects, top-level transcripts only: for each (transcript, tracked path), walk trackedFileBackups[<path>].version in jsonl line order and count adjacent pairs with version[i] < version[i-1]. Plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function bX\\(e,n,r,o\\).{0,2100}' and rg -o 'KBe *= *[0-9]+'.",
"observed": "62 transcripts carry snapshot lines; 4,061 snapshot lines; 56,940 version changes; 159 version decreases; decreases appear in exactly 4 transcripts (56, 81, 13 and 9 decreases) and in none of the other 58. Binary: the next version is 'let B=0;for(let z of d.snapshots){let fe=Ad(z.trackedFileBackups,v);if(fe&&KCe(fe.version))B=Math.max(B,fe.version);let me=qQt(fe?.backupFileName);if(me!==void 0)B=Math.max(B,me)}let U=B+1', and the retained snapshot list is capped at 'KBe=100'.",
"rule": "One reset per adjacent pair, per tracked path, per transcript, where version[i] < version[i-1] in jsonl line order. Subagent transcripts are excluded (they carry no file-history lines).",
"note": "Direction, shape and concentration all reproduce; only the totals moved with corpus growth (148 to 159 decreases, 61 to 62 transcripts), and the concentration is still exactly 4 transcripts. One mechanism refinement from the binary: the next version is the maximum version seen across the RETAINED snapshots - capped at 100 - plus one, with versions also parsed out of the @vN backup file names. Eviction past that 100-snapshot cap can therefore lower the maximum on its own, so a process restart is one route to a reset rather than demonstrably the only one. csift's rule of reading jumps only within a generation is unaffected either way."
}
]
},
{
"id": "FH-017",
"area": "freshness-file-history",
"behavior": "`isSnapshotUpdate` is not a usable change flag, but not in the direction recorded. Measured over 2,281 snapshot pairs, the flag was NEVER true without a version change - the cross-tab is (true,false) 0, (true,true) 586, (false,true) 1,278, (false,false) 417 - so a true flag implies a change while a false flag says nothing, and 1,278 real version changes carried false. Since 2026-08 the flag has been uniformly false (95 snapshot lines, 0 true), having been true 586 times between 2026-05 and 2026-07.",
"depends": "csift reads only `version` to detect a change and never consults the flag (no src/ reference to `isSnapshotUpdate` exists; it appears only in test fixtures), so its external-write instrument stays sound: a version JUMP is reported whether the flag rode true or false, and a flag-true snapshot with no version change reports nothing. Keying on the flag instead would not INVERT the detector, it would silently under-report - it would have missed the 1,278 changes that carried false against the 586 it would have caught, and detects nothing at all since 2026-08, when the flag went uniformly false.",
"code": [
{
"path": "src/files/external.rs",
"lines": "6-8",
"snippet": "//! trace. The instrument: CC snapshots every Edit/Write-tracked file per prompt and\n//! bumps `trackedFileBackups[<path>].version` only when the bytes changed; a version\n//! JUMP with no tool write of that path in the interval is an external write."
},
{
"path": "src/files/external.rs",
"lines": "65-71",
"snippet": " for (path, entry) in tfb {\n if !is_settings_family(path) {\n continue;\n }\n let Some(version) = entry.get(\"version\").and_then(serde_json::Value::as_u64) else {\n continue;\n };"
},
{
"path": "src/files/external.rs",
"lines": "84-88",
"snippet": " for (line, turn, ts, version) in seq {\n if let Some((prev_line, prev_v)) = prev {\n // A DECREASE = a generation reset (the counter restarts on process\n // restart); only same-generation jumps are readable here.\n if *version > prev_v {"
}
],
"instrument": "For each snapshot line after the first, record `isSnapshotUpdate` and whether any tracked path's version differs from the previous snapshot, then cross-tabulate. Counting rule: one row per snapshot line after the first.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "Over two project directories under ~/.claude/projects: for each snapshot line after the first in its transcript, record isSnapshotUpdate and whether any tracked path's version differs from the previous snapshot, then cross-tabulate; separately bucket the raw isSnapshotUpdate value by the month of snapshot.timestamp.",
"observed": "2,318 snapshot lines, 2,281 cross-tab rows: (isSnapshotUpdate=false, changed=false) 417, (false, true) 1,278, (true, true) 586, (true, false) 0. Raw flag values by month: 2026-05 false 162 / true 45; 2026-06 false 911 / true 299; 2026-07 false 564 / true 242; 2026-08 false 61 / true 0; 2026-09 false 34 / true 0.",
"rule": "One row per snapshot line after the first in its transcript; 'changed' means at least one tracked path's version differs from that path's version in the immediately preceding snapshot line of the same transcript; a path present in only one of the two snapshots counts as changed.",
"note": "The claim's cross-tab does not reproduce in any direction: it recorded (true,false) 51 times and concluded every version change arrives with the flag false, whereas 586 changes arrived with it true and the (true,false) cell is empty across 2,281 rows. On top of that the flag stopped being emitted as true after 2026-07, so in current Claude Code it carries no information at all. What is unaffected is the consequence the ledger entry exists to protect: csift reads only `version`, never the flag, and that remains correct - more so now, since keying on the flag today would detect nothing."
}
]
},
{
"id": "FH-018",
"area": "freshness-file-history",
"behavior": "A snapshot interval containing BOTH a tool write and a silent write produces only ONE version increment, so the version-jump rule alone cannot see the silent half; only replaying the interval's edits onto the previous snapshot content and comparing with the new snapshot content is complete.",
"depends": "csift's `files` external-write row is deliberately the incomplete-but-cheap form and `recover` carries the complete form - comparing the replayed buffer to a VERIFIED snapshot blob and rebasing on disagreement; without the rebase a reconstruction can report 100% complete for a state that never existed on disk.",
"code": [
{
"path": "src/files/external.rs",
"lines": "18-21",
"snippet": "//! within a generation, so a write hiding across a reset is not reported; a tool\n//! write and a silent write in the SAME snapshot interval merge into one bump and\n//! the silent half is invisible here (recover's content comparison is the complete\n//! form); a session that never tool-touched the file has no tracking at all."
}
],
"instrument": "Find a session with two Edits of the same tracked path between adjacent snapshot lines and confirm the version advanced by exactly one. Counting rule: one interval per adjacent snapshot pair, one increment per tracked path.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "For each adjacent snapshot-line pair in a transcript, count the structured edits (Edit/Write/MultiEdit/NotebookEdit tool_use records) whose jsonl line number falls strictly between them, grouped by target path; for each path with two or more, bucket the difference between the tracked path's version in the later and the earlier snapshot.",
"observed": "227 qualifying intervals across the scanned transcripts: bump +1 in 148, bump +0 in 79, bump of +2 or more in 0. The largest edit burst inside a single interval was 19 structured edits of one path, and it still moved the version by no more than one.",
"rule": "One interval per adjacent snapshot-line pair; edits attributed by jsonl line number strictly between the two snapshot lines; only intervals holding two or more structured edits of the SAME tracked path are counted; a relative tracked path is matched to an absolute tool path as a path-component suffix.",
"note": "Confirmed with no exception in 227 intervals: the version is a per-snapshot state marker, not an edit counter, so the number of writes inside an interval is unrecoverable from it. The 79 intervals that bumped by zero are the sharper form of the same limit - even two or more tool writes can leave the version untouched, when the interval's net effect on the bytes was nil or the backup was taken before the edits landed. Both cases make csift's reliance on content comparison rather than jump counting the only complete form."
}
]
},
{
"id": "FH-019",
"area": "freshness-file-history",
"behavior": "`backupTime` records when Claude Code backed the file up, not when the file changed, so it is a bound on the write instant rather than the write instant. The backup never precedes the write (429 of 429 lags non-negative), but the size of the lag depends on which carrier records it: a newly tracked file's first backup rides a `file-history-delta` written at the moment of the write (146 measurements, median 0.25 s, max 3.20 s), while a later bump of an already tracked file waits for the next snapshot (283 measurements, min 29.9 s, median 1,409 s).",
"depends": "csift reports the version transition plus the surrounding INTERVAL rather than an instant, so a consumer never reads the backup time as the write time; presenting `backupTime` as the mutation instant would misdate every silent write.",
"code": [
{
"path": "src/recover/types.rs",
"lines": "80-91",
"snippet": " /// A `file-history-snapshot` recorded a disk backup of `--file` at this time.\n /// Since v0.9.4 the marker carries its INSTRUMENT payload: the per-path\n /// `version` (monotone within a GENERATION; the counter resets mid-session on\n /// process restart - 148 real occurrences - so a DECREASE means a new\n /// generation, never an anomaly), the store `backupFileName`, its `backupTime`,\n /// and - attached by the scan post-pass ONLY on a version CHANGE, and only when\n /// the store file exists AND its mtime agrees with `backupTime` (the @vN name\n /// COLLIDES across a generation reset, so an unverified read can return the\n /// wrong generation's bytes) - the snapshot CONTENT. The replay layer uses a\n /// verified content to detect and REBASE across harness-side writes that left\n /// no tool record (e.g. Claude Code rewriting settings.json on /model); an\n /// unverified marker stays the old coverage annotation."
}
],
"instrument": "In a session where a slash command rewrote settings, compare the command record's timestamp with the following snapshot entry's `backupTime`; expect the backup to trail by seconds. Counting rule: one lag measurement per version change with a nearby command record.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Snapshot form: for each version change of a tracked path between adjacent snapshot lines, measure the seconds from the latest structured edit of that path inside the interval to the new entry's backupTime. Delta form: for each file-history-delta line, measure the seconds from the latest preceding structured edit of the same path to the delta's timestamp. Plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '(async )?function XQt\\(.{0,600}'.",
"observed": "Snapshot form, 283 measurements: min 29.9 s, median 1,409 s, max 338,710 s, negative 0. Delta form, 146 of 148 delta lines matched to a preceding structured write of the same path: min 0.02 s, median 0.25 s, p90 0.39 s, max 3.20 s, all 146 under 5 s; every one of the 148 deltas carried version 1 and realParentDir. Binary: 'async function XQt(e,n){...await rQe(e,o);...return{backupFileName:r,version:n,backupTime:new Date,realParentDir:f}}' - backupTime is stamped when the copy is made.",
"rule": "One lag measurement per version change that has at least one structured edit of the same path inside its interval; one delta measurement per delta line with a preceding structured edit of the same path; lag is backupTime minus the edit record's timestamp, so a negative value would mean the backup preceded the write.",
"note": "The reason the entry gives is right and is now confirmed in the binary - the backup routine stamps backupTime with new Date() at the moment it copies the file. The 'roughly 13 seconds' figure is one incident, not a general lag, and there is no single characteristic value: the two carriers differ by three orders of magnitude. csift's practice of reporting the transition plus the surrounding interval rather than an instant is what makes both carriers safe to consume."
}
]
},
{
"id": "FH-020",
"area": "freshness-file-history",
"behavior": "The file-history tracked set is populated by STRUCTURED writes only - in one measured session tracked paths numbered 113, structurally-written paths 113, and tracked-but-never-structurally-written 0 - so a bash-only write never creates a tracked entry and bash writes never enter the store.",
"depends": "csift never expects a snapshot boundary for a bash-written file and instead discloses bash mutations through per-window opaque accounting; assuming coverage from the store would report a false clean window for bash-heavy sessions.",
"code": [
{
"path": "src/recover/scan.rs",
"lines": "321-327",
"snippet": "/// Collect the SCOPE-ACCOUNTING commands of one transcript: every mutating-CLASS\n/// marker a Bash command yields (`fmt:cargo`, `interp:python`, `pkg:npm`,\n/// `extract:tar`, `git:<sub>` - commands that mutate files they never name), plus\n/// every `PowerShell` tool call (its command text is never lexically parsed, so any\n/// file it touched is invisible; see AGENTS 3.9). Target-independent by nature: these\n/// commands CANNOT be joined to a `--file`, which is exactly why they are counted and\n/// disclosed per window instead of silently ignored."
}
],
"instrument": "For one session, build the set of `trackedFileBackups` keys and the set of Edit/Write `filePath` values; expect the tracked set to be a subset of the structured-write set. Counting rule: one path per distinct string in each set.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Over two project directories under ~/.claude/projects: build the union of trackedFileBackups keys across every snapshot line of a top-level transcript, and the set of Edit/Write/MultiEdit/NotebookEdit file_path values from that transcript and from every one of its subagent transcripts (journal.jsonl excluded), then count tracked paths matching no structured write.",
"observed": "810 distinct tracked paths across the 37 top-level transcripts carrying snapshots in those two directories; 0 tracked paths unmatched by a structured write. A first pass reported 5 apparent exceptions; a per-path tool census showed all five had in fact been written by Edit or Write in the same transcript, and the exceptions were an artefact of the matcher, not of the data.",
"rule": "One path per distinct trackedFileBackups key; one structured write per distinct file_path (or notebook_path) on an Edit/Write/MultiEdit/NotebookEdit tool_use. A relative tracked path counts as written when it is a path-component suffix of some absolute tool path - the leading './' may be stripped but the leading dot of a dotfile may NOT, which is exactly the bug that produced the 5 phantom exceptions.",
"note": "Confirmed at roughly seven times the scale of the original measurement, with zero exceptions. Two operational cautions for anyone rerunning it. The tracked set of a top-level session includes files its SUBAGENTS structurally wrote, so a comparison against the top-level transcript alone will show false exceptions. And matching relative tracked paths against absolute tool paths needs a path-component suffix test that preserves dotfile names; a naive strip of leading '.' and '/' characters silently unmatches every dotfile and every path under a dot-directory."
}
]
},
{
"id": "FH-021",
"area": "freshness-file-history",
"behavior": "The tracked-path set spans ordinary source files at scale: a census over the 59 transcripts carrying file-history snapshots found 1,739 distinct tracked-path keys against 11 settings-family keys (0.6%), and only 5 of those 59 transcripts (8.5%) tracked any settings file at all. Under a 200 MB transcript cutoff the same census reads 1,227 keys, 3 settings-family (0.2%), 3 of 56 transcripts (5.4%).",
"depends": "csift scope-limits external-write reporting to basenames `settings.json` and `settings.local.json` under a `.claude` parent precisely because wider reporting would flood every `files` timeline; the instrument is per-session evidence, not a corpus-wide sensor.",
"code": [
{
"path": "src/files/external.rs",
"lines": "13-14",
"snippet": "//! listing \"harness writes\" beyond settings would flood every timeline (measured\n//! corpus: 1701 tracked paths, 11 settings-family)."
}
],
"instrument": "Collect the union of `trackedFileBackups` keys corpus-wide and count how many match the settings-family shape; separately count sessions with at least one such key. Counting rule: one path per distinct key string, one session per transcript file.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "src/files/external.rs comment; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' -t harness.meta.snapshot --raw --max-count 0 (corpus-wide dump of every file-history-snapshot / file-history-delta line), piped into a python3 census that unions the `snapshot.trackedFileBackups` keys and classifies each with csift's own is_settings_family rule (basename settings.json|settings.local.json AND immediate parent `.claude`); csift search '' -t harness.meta.snapshot -l --max-count 0 | wc -l for the denominator; csift search '\\.claude/settings(\\.local)?\\.json' -t harness.meta.snapshot -l --max-count 0 | wc -l for the numerator; a second python3 pass re-ran the same census split by transcript byte size against the 200 MB cutoff.",
"observed": "5,490 raw lines returned (3,951 file-history-snapshot + 1,539 file-history-delta; 3,930 distinct snapshot records; 4 malformed lines skipped, reported on stderr). Union of tracked-path keys = 1,739, of which 11 are settings-family = 0.6%. 59 transcripts carry snapshot records; 5 of them (8.5%) track at least one settings file. Restricted to transcripts under 200 MB: 56 transcripts, 1,227 tracked-path keys, 3 settings-family (0.2%), 3 of 56 (5.4%) tracking a settings file. The largest top-level transcript in the corpus is 687.5 MB, so the 200 MB cutoff is not vacuous. Code site verified verbatim: src/files/external.rs:10-14 and 25-32 match the claim's snippets character for character.",
"rule": "One path per distinct trackedFileBackups key string (keys are counted verbatim, so a repo-relative spelling and an absolute spelling of the same file are two keys - this is the same rule csift's scope law applies). One session per transcript file. A transcript 'tracks a settings file' iff at least one snapshot record in it carries a settings-family key. Percentages are settings-family keys over all keys, and settings-tracking transcripts over snapshot-carrying transcripts.",
"note": "The scope law it justifies is untouched - the ratio moved from 0.65% to 0.6%, which strengthens rather than weakens the argument for reporting only the settings family. The claim's two halves were measured at two different scopes: 1,701/11 is the unfiltered union while '4 of 58 sessions' carried a 200 MB cutoff. Reported both scopes above so the next reader does not have to guess. The settings-family count staying at exactly 11 across a corpus that grew by 38 tracked paths is consistent with settings files being tracked only when a session tool-edited one."
}
]
},
{
"id": "FH-022",
"area": "freshness-file-history",
"behavior": "Claude Code rewrites its own settings files IN-PROCESS - /model, /config, theme changes, permission always-allow, plugin toggles - with NO tool record: corpus-wide, 44 of 62 settings mutations carry no tool write in their snapshot interval by csift's own external_write instrument (71%), 47 of 62 by an independent replication (76%). Silent writes are the MAJORITY of settings mutations, not half.",
"depends": "the per-prompt file-history snapshot is the only durable instrument, so `files` emits an `external write` timeline row from a version jump with no tool write in the interval and `recover` treats a content disagreement as a hard boundary; without it half of all settings mutations are invisible to every csift surface and a replayed settings file reports a state that never existed on disk.",
"code": [
{
"path": "src/files/external.rs",
"lines": "1-8",
"snippet": "//! Settings-family external writes: the file-history snapshot instrument applied to\n//! the `files` timeline.\n//!\n//! Claude Code rewrites its settings files IN-PROCESS (/model, /config, theme,\n//! permission \"always allow\", plugin toggles) - no tool record, usually no printed\n//! trace. The instrument: CC snapshots every Edit/Write-tracked file per prompt and\n//! bumps `trackedFileBackups[<path>].version` only when the bytes changed; a version\n//! JUMP with no tool write of that path in the interval is an external write."
}
],
"instrument": "Over a session that tracks `~/.claude/settings.json`, list every version jump and check whether any Edit/Write tool_use naming that path falls in the interval; expect roughly half with none. Live: change a setting through the harness, then `csift files @main --by timeline --format json | jq 'select(.op==\"external_write\")'`. Counting rule: one mutation per version jump, classified silent or tool-accompanied.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md sections 6.6 and 6 v0.9.4 ledger; CHANGELOG 0.9.4; src/files/external.rs comment; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "For each of the 5 transcripts that track a settings file: csift files @<session> --by timeline --format json | rg -c '\"op\":\"external_write\"' ; plus an independent python3 replication that walks each transcript in line order, builds the per-path version sequence from snapshot.trackedFileBackups, counts every version INCREASE as one settings mutation, and marks it silent when no Edit/Write/MultiEdit/NotebookEdit tool_use naming that path (suffix-tolerant match between the relative and absolute spelling) falls in the open line interval (prev_snapshot_line, this_snapshot_line]. Binary side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'trackedFileBackups|backupFileName|file-history-snapshot|file-history-delta|isSnapshotUpdate|backupTime|realParentDir'.",
"observed": "62 settings-family version jumps in total across the 5 transcripts (3 + 32 + 5 + 15 + 7). csift's own instrument emits 44 external_write rows over the same 5 transcripts (71%). The independent replication classifies 47 of 62 as silent (76%) and 15 as tool-accompanied (24%); the 3-row gap is csift additionally crediting its bash-heuristic mutations inside the interval. Exactly 1 generation reset (a version DECREASE) occurred on a settings path, so the jump count is essentially reset-free. Binary tokens present in 2.1.258: trackedFileBackups 3, backupFileName 7, file-history-snapshot 5, file-history-delta 5, isSnapshotUpdate 3, backupTime 2, realParentDir 2 string-table lines.",
"rule": "One mutation per version INCREASE of one settings-family key between two consecutive snapshot records in the same transcript; a version DECREASE is a generation reset and is not a mutation. A mutation is silent iff no structured file-writing tool_use naming that path appears on a jsonl line strictly after the previous snapshot line and at or before this one. Percentages are silent mutations over all mutations.",
"note": "The mechanism half of the claim is confirmed and the total (62 mutations) reproduces exactly; only the silent fraction moved, and it moved AGAINST the claim's conservatism - 71-76% rather than 'half'. The clause 'usually no printed trace: 4 of 5 silent writes printed nothing a string table could match' was NOT rerun: deciding it requires driving five real setting changes through a live harness and watching the terminal, which mutates the operator's settings files and is outside what a read-only verifier may do. What would decide it: a throwaway --claude-home / CLAUDE_CONFIG_DIR home, five interactive setting changes through /model, /config, theme, an always-allow grant and a plugin toggle, and a transcript-plus-terminal capture of each."
}
]
},
{
"id": "FH-023",
"area": "freshness-file-history",
"behavior": "Claude Code's own file-history checkpoint store lives at `<claude-home>/file-history/<top-level-session-uuid>/<hash>@v<N>`, where `<hash>` is the first 16 hex chars of the sha256 of the ABSOLUTE file path and the file content is the PLAIN checkpoint snapshot with no JSON wrapper.",
"depends": "`recover --list-backups` computes the key without needing any transcript record, so a wrong hash input (a relative path, a plan sigil) finds nothing at all rather than the wrong file.",
"code": [
{
"path": "src/recover/backups.rs",
"lines": "1-7",
"snippet": "//! `recover --list-backups`: list Claude Code's OWN file-history checkpoint store for\n//! one absolute path.\n//!\n//! Store layout (verified against the live store): `<claude-home>/file-history/\n//! <top-level-session-uuid>/<hash>@v<N>`, where `<hash>` is the first 16 hex chars of\n//! sha256(absolute file path) and the file content is the PLAIN checkpoint snapshot\n//! (no JSON wrapper). Facts that bound what a listing may claim:"
},
{
"path": "src/recover/backups.rs",
"lines": "36-40",
"snippet": "/// The store key: sha256 of the absolute path, first 16 hex chars.\nfn path_hash(path: &str) -> String {\n let digest = Sha256::digest(path.as_bytes());\n digest[..8].iter().map(|b| format!(\"{b:02x}\")).collect()\n}"
}
],
"instrument": "`printf '%s' '<abs path>' | shasum -a 256 | cut -c1-16` then `ls ~/.claude/file-history/*/<that hash>@v*`, and compare with `csift recover <target> --file <abs path> --list-backups --format json | jq 'select(.kind==\"backup\")'`. Counting rule: one row per store file matching the hash prefix.",
"located": {
"claude_code": "2.1.246",
"csift": "0.8.1",
"source": "SPEC.md sections 6.7 and 6 v0.8.1 ledger; CHANGELOG 0.8.1; src/recover/backups.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{300}@v\\$\\{.{200}' and rg -o '.{200}file-history.{300}' for the store-path builder; on disk: a python3 pass recomputing sha256(realParentDir + '/' + tracked key)[:16] for live trackedFileBackups entries and comparing it with the record's own backupFileName; printf '%s' \"$PWD/src/recover/backups.rs\" | shasum -a 256 | cut -c1-16 from the csift checkout, then find ~/.claude/file-history -name '<hash>@v*'; wc -c and diff of the located blob against the working-tree file; python3 -c 'json.load(open(blob))'; find ~/.claude/file-history -type f | rg -v '/[0-9a-f]{16}@v[0-9]+$'.",
"observed": "2.1.258 contains the name builder verbatim: function O3r(e,n){return`${T3r(\"sha256\").update(e).digest(\"hex\").slice(0,16)}@v${n}`} and the producer regex var D3r=/^[0-9a-f]{16}@v\\d+$/. The store path builder is case\"fileHistory\":return r(t,\"file-history\",n.sessionId,n.backupFileName) with function uH(e,n){...return O8(r,\"file-history\",n||Q(),e)}. Live records confirm the hash input is the ABSOLUTE path even when the key is relative: a key spelled `src/path.rs` whose realParentDir ends in `/src` carries a 16-hex backupFileName token, and sha256 of the JOINED absolute path reproduces that token exactly, character for character; an independently, absolutely-spelled key under ~/.claude/plans likewise reproduces its own recorded token. (The digest values themselves are withheld here: a store hash is a one-way digest of a home-directory path and publishing it would let a reader confirm a guessed home path by brute force. Both are recomputable in one command by the rule below.) The csift-checkout probe: the recomputed hash for the working-tree file src/recover/backups.rs resolved to exactly ONE blob in the whole store, at ~/.claude/file-history/<session-uuid>/<hash>@v2, 8,609 bytes, byte-identical to the working-tree src/recover/backups.rs (diff empty, wc -c equal) and not parseable as JSON (json.load raises JSONDecodeError). 6,554 of 6,555 store files match ^[0-9a-f]{16}@v[0-9]+$ (the one exception is a .DS_Store). 52 store directories, all 52 uuid-shaped; 34 resolve to a top-level transcript, 0 to a subagent transcript, 18 to no transcript now on disk. Code sites verified verbatim: src/recover/backups.rs:1-7 and 36-40.",
"rule": "One row per store file matching <16 hex>@v<digits>. A hash is confirmed iff sha256 of the absolute path, first 16 hex chars, equals the leading token of a store file name or of a record's backupFileName. 'Plain snapshot, no JSON wrapper' is confirmed iff the blob bytes equal the source file's bytes and json.load raises.",
"note": "Refutation attempt failed in three independent directions: the binary's own name builder, the live records' backupFileName field, and a fresh hash computed from a csift source path all agree. The strongest refutation attempt was to look for a store directory keyed on a subagent id (which would break the 'top-level-session-uuid' half) - zero of 52 store directories are subagent-shaped. One forward-compat observation that does not change the claim: the storage-key VALIDATOR in 2.1.258 is JQ=/^[0-9a-f]{16}(?:[0-9a-f]{48})?@v\\d+$/, tolerating a full 64-hex-char digest form, while the writer always emits slice(0,16). No 64-hex file exists in the live store, so csift's 16-char assumption is safe today; a future switch to the long form would be the thing that breaks it."
}
]
},
{
"id": "FH-024",
"area": "freshness-file-history",
"behavior": "The store's write path is TOOL-LAYER ONLY (the rewind feature): bash writes and manual edits never land there - 98% of structured Edit/Write targets are in the store versus 0.4% of bash-only written paths - so absence proves nothing about a file's history. The store is PRUNED: only the newest 100 snapshots are retained and the backups they no longer reference are deleted (12% of all recorded backup names are already gone), but version 1 is EXEMPT from eviction by an explicit guard, so @v1 is almost always still present (1 of 623 recorded @v1 names missing).",
"depends": "`recover --list-backups` lists the store with those provenance bounds printed and csift never merges checkpoint content into a reconstruction - a checkpoint has no transcript anchor, and the transcript stays the ground truth.",
"code": [
{
"path": "src/recover/backups.rs",
"lines": "10",
"snippet": "//! - the store is PRUNED: old checkpoints get deleted wholesale (`@v1` often gone);"
}
],
"instrument": "List a session's store directory and compare its entries with the session's structured Edit/Write targets and with its bash-written paths: bash-only paths are absent, and the lowest surviving version is frequently above v1. Counting rule: one row per store file, one path per distinct tool target.",
"located": {
"claude_code": "2.1.246",
"csift": "0.8.1",
"source": "CHANGELOG 0.8.1; src/recover/backups.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "On disk: a python3 pass over 8 sessions that have both a store directory and a transcript, comparing sha256(path)[:16] of every absolute path in csift files @<session> --by timeline --format json --no-subagents against the hash set in that session's store directory, split by the row's own `heuristic` flag (false = structured Edit/Write, true = bash-derived). A second python3 pass collected every backupFileName that a transcript actually RECORDED and checked whether the blob still exists in that session's store directory. Binary: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function A3r\\(.{0,600}' for the evictor, plus the same filter compared across ~/.local/share/claude/versions/{2.1.229,2.1.241,2.1.251,2.1.257,2.1.258}.",
"observed": "TOOL-LAYER ONLY holds hard: over the 8 sessions, 869 of 885 structured Edit/Write target paths (98%) are present in that session's store directory, versus 4 of 980 bash-only written paths (0.4%). PRUNING is real but bounded: of 6,535 backupFileName values recorded across the 59 snapshot-carrying transcripts, 769 (12%) are no longer on disk. The '@v1 often gone' clause is REFUTED: of the 623 recorded @v1 names, exactly 1 is missing (0.2%). The evictor explains why - async function A3r(e,n,r){...for(let _ of e)for(let v of Object.values(_.trackedFileBackups))if(v.backupFileName!==null&&!d.has(v.backupFileName)&&v.version!==1&&D3r.test(v.backupFileName))f.add(v.backupFileName)... - version 1 is explicitly exempt from eviction, and the same `version!==1` guard is present in 2.1.229, 2.1.241, 2.1.251 and 2.1.257 as well, so this is a long-standing exemption and not a change. The retention constant is KBe=100: snapshots beyond the newest 100 are dropped and their unreferenced, non-v1 backups deleted. The signal that likely produced the wrong clause is real but means something else: 2,088 of 2,770 (session-dir, path-hash) groups (75.4%) have a lowest surviving version above v1 - because the version counter is carried forward across a resume (the next version is max over the retained snapshot history plus one), so v1 was never WRITTEN in that directory rather than pruned from it. Code sites verified verbatim: src/recover/backups.rs:7-16.",
"rule": "One path per distinct absolute path in a session's files timeline, classified structured vs bash-only by the row's `heuristic` flag with bash-only defined as bash-derived and never a structured target in the same session; present-in-store iff sha256(path)[:16] appears as the leading token of any file in that session's store directory. For pruning: one row per backupFileName string a transcript recorded, counted missing iff no file of that name exists in that session's store directory.",
"note": "The claim's load-bearing half - absence proves nothing, so csift may only LIST the store - survives the strongest refutation available (a 980-path bash-only sample produced 4 hits, and even those are explainable as paths tool-edited in a session whose backups were hard-linked in on resume). The failing half is a mis-inference rather than drift: the `version!==1` exemption predates the CC version the claim was located at, so 'often gone' was never true; what is true is that a file's FIRST backup in a given session directory is usually not v1, because the counter carries across a resume."
}
]
},
{
"id": "FH-025",
"area": "freshness-file-history",
"behavior": "The @vN token is NOT an ordering key: the counter resets per session directory and is reused across directories, and because it also resets mid-session the names COLLIDE inside ONE session directory - measured, 418 names reused for two or more distinct backupTime instants, and of the 297 with a surviving blob, 183 blob mtimes belong to the LATEST generation and 0 to an earlier one, the earlier generation's blob having been overwritten at the same name. Only the backup instant - the store file's mtime, checked against the marker's backupTime - identifies the generation. Separately, a blob can show a link count of 2 because the session-restore path HARD-LINKS a prior session's backups into the new session directory under the same name (119 such inode groups, every one spanning two directories, none inside one).",
"depends": "csift orders backups by the store file's mtime and trusts a blob's bytes only when that mtime agrees with the marker's recorded `backupTime` inside a 120-second window, otherwise degrading to a content-less marker; reading `@vN` by name alone would splice the wrong generation's bytes into a `recover` rebase.",
"code": [
{
"path": "src/recover/snapshots.rs",
"lines": "11-12",
"snippet": "//! the `@vN` file name COLLIDES across a mid-session generation reset (the version\n//! counter restarts; measured 148 resets), so an unverified read can silently"
}
],
"instrument": "For a session whose transcript shows a version decrease, `ls -l` its store directory and compare blob mtimes with the `backupTime` values in the transcript: expect at least one `@vN` name whose mtime belongs to the later generation. `csift recover <target> --file <path> --list-backups --format json | jq -r '.backup_time'` prints the order the version numbers do not follow. Counting rule: one row per store file.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.1",
"source": "SPEC.md sections 6.7 and 6 v0.8.1 ledger; src/recover/backups.rs and src/recover/snapshots.rs module docs; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 passes over the 59 snapshot-carrying transcripts (ids from csift search '' -t harness.meta.snapshot -l --max-count 0 ): (a) count version DECREASES per tracked path per transcript = generation resets; (b) group by backupFileName and flag any name carrying more than one distinct backupTime = an in-directory @vN collision; (c) for each colliding name with a surviving blob, compare the blob's filesystem mtime against every recorded backupTime and record which one it falls within 120s of; (d) the same mtime-vs-backupTime comparison over all NON-colliding surviving blobs, for the trust rule's base rate; (e) an os.stat nlink census over every store blob, grouping links by inode. Binary: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function \\$3r\\(.{0,420}' and rg -o '(link|linkSync) as [A-Za-z0-9_$]{2,6}'.",
"observed": "182 generation resets across 4 transcripts (the code doc says 148; same rule, larger corpus). 418 backupFileName values are reused for two or more distinct backupTime instants INSIDE one session directory, spread over 5 transcripts; 297 of those still have a blob on disk (121 do not). Of the 297: 183 blob mtimes fall within 120s of the LATEST recorded backupTime, 0 fall within 120s of any EARLIER one, and 114 match none - i.e. the later generation overwrote the earlier generation's blob at the same name in every measurable case, and reading @vN by name alone would return the later generation's bytes. Concrete instance: one @v3 name (digest withheld, see FH-023) carries backupTime 2026-08-24T09:15:14.514Z and 2026-09-02T09:02:14.261Z, and the surviving blob's mtime is within 0.0s of the LAST one. The 120s window is well calibrated: over 5,472 non-colliding surviving blobs, 4,751 (86.8%) agree with backupTime to within 1s, 154 more within 120s, and 567 (10.4%) exceed the window and are refused. Hard links: 119 inodes carry nlink 2 (238 blobs of 6,554); all 119 groups span exactly two DIFFERENT session directories and all 119 share the same @vN name; 0 groups sit inside a single session directory. The binary names the cause - the restore path async function $3r(e,n,r){let d=uH(e,n),f=O8(r,e)...await C3r(d,f)} with `link as C3r`, i.e. resume/restore hard-links a prior session's backups into the new session directory under the same name (the ordinary backup write uses `copyFile as rQe`). Code sites verified verbatim: src/recover/backups.rs:10-12, src/recover/snapshots.rs:5-14 and 18-21 (MTIME_TOLERANCE_SECS = 120).",
"rule": "One reset per version DECREASE of the same tracked path between consecutive snapshot or delta records in one transcript. One collision per backupFileName that carries two or more distinct backupTime values within one transcript. A blob's generation is decided by the recorded backupTime nearest its mtime, and only when that distance is 120s or less. One inode group per distinct st_ino among store blobs with st_nlink > 1.",
"note": "This is the strongest-confirmed claim of the five and the instrument sharpened it: the directional result (0 of 297 colliding blobs belonging to an EARLIER generation, 183 to the latest) means reading @vN by name is not merely unordered but systematically returns the newest generation's bytes, so the failure mode csift guards against is the one that actually occurs. The 120s tolerance is empirically the right size - 86.8% of agreements land inside 1s, so widening it would buy almost nothing while admitting collisions. One sub-clause needed correcting: hard links are not a content-dedupe of identical blobs but the restore path's link() into a resumed session's directory; the observable consequence for csift (a blob reachable from two session directories) is unchanged."
}
]
},
{
"id": "FH-026",
"area": "freshness-file-history",
"behavior": "A TEXT-branch Read result (toolUseResult.type == \"text\") is persisted as toolUseResult.file = {filePath, content, numLines, startLine, totalLines} plus an OPTIONAL sixth key truncatedByTokenCap, set when a WHOLE-FILE read was auto-paginated for exceeding the output token cap - content then holds only the partial FIRST page. The type qualifier is load-bearing: the other Read branches carry their own file shapes and none of the five text keys is guaranteed there (file_unchanged carries filePath alone; parts carries count/filePath/originalSize/outputDir; pdf carries base64/filePath/originalSize).",
"depends": "csift's `push_read_event` decides full-snapshot versus partial splice from line arithmetic alone (`startLine == 1 && numLines >= totalLines`) and never reads the flag, so a capped read normally lands as the conservative partial splice with explicit gaps - but `recover --coverage` cannot name the token cap as the reason, and that arithmetic is the only thing standing between a capped page and a full-file anchor.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "178-182",
"snippet": " let lines: Vec<String> = split_lines(content);\n let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;\n if is_full {"
},
{
"path": "src/recover/types.rs",
"lines": "17-22",
"snippet": " /// Full ground-truth content (an anchor): a Write result, a full Read\n /// (`startLine==1 && numLines==totalLines`), or a `file` attachment.\n FullSnapshot {\n content: String,\n total_lines: usize,\n source: SnapSource,"
}
],
"instrument": "`strings -a <the Claude Code 2.1.258 executable> | grep -o 'truncatedByTokenCap:M().optional().describe(\"[^\"]*\"'` prints the quoted sentence verbatim (counting rule: one hit per schema literal). Corpus: over every jsonl under ~/.claude/projects count echoes carrying the key, one observation per Read echo - measured 2026-09-02: 174.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": "2.1.150",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -a -n 6 ~/.local/share/claude/versions/2.1.258 > /tmp/cc258.strings; python3 -c \"import re;s=open('/tmp/cc258.strings',errors='replace').read();print(re.search(r'file:c\\\\(\\\\{filePath.{0,700}',s).group(0)[:700])\" AND python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects')\nks=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n for f in fn:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if '\\\"filePath\\\"' not in line or 'toolUseResult' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if isinstance(t,dict) and isinstance(t.get('file'),dict):\n ks[(t.get('type'),tuple(sorted(t['file'].keys())))]+=1\nprint(ks)\nEOF AND grep -rn 'truncatedByTokenCap' src/ (run in the csift repo)",
"observed": "Binary schema verbatim: file:c({filePath:i().describe(\"The path to the file that was read\"),content:i().describe(\"The content of the file\"),numLines:A().describe(\"Number of lines in the returned content\"),startLine:A().describe(\"The starting line number\"),totalLines:A().describe(\"Total number of lines in the file\"),truncatedByTokenCap:M().optional().describe(\"True when a whole-file read was auto-paginated because it exceeded the token cap (the content is a partial first page). A programmatic signal for internal consumers; survives output reconstruction (unlike the render-time banner).\") -- the claim's quoted sentence is present word for word. Corpus over 7,817 jsonl files: 8,758 records carry a dict toolUseResult.file. Split by toolUseResult.type: type=\"text\" -> 8,434 with the five keys ('content','filePath','numLines','startLine','totalLines') and 174 with the same five plus 'truncatedByTokenCap' (the claim's 174, matched exactly). The remaining 150 are NON-text Read branches with entirely different file shapes: type=\"file_unchanged\" -> ('filePath',) x120, type=\"parts\" -> ('count','filePath','originalSize','outputDir') x26, type=\"pdf\" -> ('base64','filePath','originalSize') x4. In the csift repo `grep -rn 'truncatedByTokenCap' src/` returns nothing, so the depends clause (\"never reads the flag\") is confirmed.",
"rule": "One observation per record whose toolUseResult.file is a dict, keyed by (toolUseResult.type, sorted tuple of file's keys); recursive over ~/.claude/projects, subagent transcripts included. Binary: one hit per schema literal.",
"note": "The behavior and the 174 count survive intact; only the scope needed narrowing. 1.7% of file-bearing Read echoes (150 of 8,758) are non-text branches whose file object does not have the enumerated shape, so a consumer that reads numLines/startLine/totalLines without first checking toolUseResult.type would read absent keys. csift is unaffected: push_read_event is only reached from the text branch and takes num_lines/total_lines as Options. code-site note: Both sites confirmed verbatim at the claimed paths and line numbers: src/recover/carriers.rs 178-182 and src/recover/types.rs 17-22. The function containing the arithmetic is push_read_event (src/recover/carriers.rs:167)."
}
]
},
{
"id": "FH-027",
"area": "freshness-file-history",
"behavior": "`truncatedByTokenCap` is spread into the result object only as the literal `true` - the construction site is `...Ue!==void 0&&{truncatedByTokenCap:!0}` where `Ue` is the truncation banner - so the key is never written `false` and its absence is the only way `false` is expressed.",
"depends": "Any csift consumer of the key must treat absence as false rather than expecting an explicit `false`; the pattern matches the absence-means-false convention csift already relies on elsewhere, and a future `recover` gate keying on `== Some(false)` would silently never fire.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "179-181",
"snippet": " let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;"
}
],
"instrument": "`grep -o '.\\{450\\}truncatedByTokenCap.\\{450\\}'` over `strings -a` of the 2.1.258 binary shows the single construction site with the `{truncatedByTokenCap:!0}` spread. Corpus check: over every jsonl under ~/.claude/projects, count the distinct JSON values of the key, one observation per echo carrying it - measured now: 174 occurrences, value `true` on 174, `false` on 0.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": "2.1.150",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 - <<'EOF'\nimport re\nsrc=open('/tmp/cc258.strings',errors='replace').read()\ni=src.find('truncatedByTokenCap:!0')\nseg=src[max(0,i-3000):i+120]\nfor m in re.finditer(r'Ue\\\\s*=\\\\s*[^;,]{0,200}', seg): print('ASSIGN:', m.group(0))\nprint(seg[-450:])\nprint('consumer tests:', src.count('truncatedByTokenCap===!0'))\nEOF AND the same whole-corpus walk as FH-026, tallying json.dumps(file['truncatedByTokenCap'])",
"observed": "Single construction site, verbatim: let ht={type:\"text\",file:{filePath:n,content:De,numLines:Ne,startLine:Ue!==void 0?Math.max(1,f):f,totalLines:ke,...Ue!==void 0&&{truncatedByTokenCap:!0}},...pt&&{artifactRead:pt}}; -- the spread is exactly the claimed ...Ue!==void 0&&{truncatedByTokenCap:!0}. Ue is confirmed to be the truncation banner by its own assignment in the same scope: Ue=!je&&Ne<ke?lfe+`${r}: showing lines 1-${Ne} of ${ke} total ...`:lfe+`${r}: showing the first ${nt.length} of ${me.length} characters ...` where lfe=\"[Truncated: PARTIAL view \\u2014 \". Consumers read it as an identity test against true, at 3 sites (U.data.file.truncatedByTokenCap===!0 twice, nt.toolUseResult?.file?.truncatedByTokenCap===!0 once) plus one d?.truncatedByTokenCap!==!0 early-return. Corpus: 174 records carry the key; distinct JSON values = {true: 174}, false: 0.",
"rule": "One observation per Read echo carrying file.truncatedByTokenCap, bucketed by the key's serialized JSON value; whole corpus. Binary: one hit per construction/consumption site.",
"note": "Confirmed on both instruments and reinforced by Claude Code's own consumers, which all test ===!0 rather than reading a boolean - the same absence-means-false convention the claim predicts. The identical spread idiom governs three sibling keys on the write/edit carriers (memdirStamped, gitDiff, staleRecovered), so the convention is systematic, not incidental to this one field. code-site note: src/recover/carriers.rs 179-181 confirmed verbatim."
}
]
},
{
"id": "FH-028",
"area": "freshness-file-history",
"behavior": "The two forms are SUCCESSIVE, not concurrent, and neither current one is pathless. Through 2.1.202 the banner was injected into the rendered tool_result as '<system-reminder>[Truncated: PARTIAL view \\u2014 showing lines 1-N of T total ...]' - pathless, 504 occurrences, none after 2.1.202. From about 2.1.206 the carrier moved to a separate read_truncation_notice attachment whose banner ALWAYS names the absolute path (183 of 183), and no copy is written into the tool_result at all. 2.1.258 additionally builds a THIRD, path-bearing sentence at reconstruction time (function qes): '[Truncated: PARTIAL view \\u2014 <abs path>: showing N of T lines. Call Read with offset/limit to page through. ...]' or, when the file cannot be paginated by line, '... <abs path>: this view is incomplete and the file cannot be paginated by line. ...' - injected into the reconstructed payload, guarded against double-application, and never persisted (0 corpus records).",
"depends": "csift parses no banner text on any surface - `rg -c \"PARTIAL view|read_truncation_notice\" src/ tests/` matches nothing - so all three grammars, the carrier migration and the path/pathless split are inert for it. Its partial-read instrument is the STRUCTURED read coordinates instead: `toolUseResult.file` and the `file` attachment supply `startLine` / `numLines` / `totalLines`, and `push_read_event` classifies a Read as a FullSnapshot only when `start_line == 1 && observed >= total && total > 0`, else as a PartialRead. The exposure this claim guards is therefore not the banner wording but the absence of coordinates: a truncated view delivered with no `numLines` / `totalLines` falls back to `total = observed` and classifies FULL, so a partial view would be replayed as a complete file.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "193-204",
"snippet": " } else {\n events.push(FileEvent {\n line_no,\n turn_index,\n timestamp_utc: ts.clone(),\n kind: EventKind::PartialRead {\n start_line: start_line.max(1),\n lines,\n total_lines: total,\n },\n });\n }"
},
{
"path": "src/recover/carriers.rs",
"lines": "64-75",
"snippet": " let start_line = file\n .get(\"startLine\")\n .and_then(serde_json::Value::as_u64)\n .unwrap_or(1) as usize;\n let total_lines = file\n .get(\"totalLines\")\n .and_then(serde_json::Value::as_u64)\n .map(|n| n as usize);\n let num_lines = file\n .get(\"numLines\")\n .and_then(serde_json::Value::as_u64)\n .map(|n| n as usize);"
},
{
"path": "src/recover/carriers.rs",
"lines": "178-181",
"snippet": " let lines: Vec<String> = split_lines(content);\n let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;"
}
],
"instrument": "Corpus: for every line containing `Truncated: PARTIAL view`, split on the em-dash-space and test whether the remainder starts with `showing`; count separately for `attachment.banner` and for `tool_result` content. One observation per carrier occurrence. Measured now: attachment banners 170 path-bearing / 0 pathless; tool_result copies 4 path-bearing / 528 pathless.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects'); EMD='\\u2014 '\npos=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n for f in fn:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if 'PARTIAL view' not in line: continue\n try: o=json.loads(line)\n except Exception: continue\n c=(o.get('message') or {}).get('content')\n texts=[]\n if isinstance(c,list):\n for b in c:\n if isinstance(b,dict) and b.get('type')=='tool_result':\n cc=b.get('content')\n if isinstance(cc,str): texts.append(cc)\n elif isinstance(cc,list): texts+=[s['text'] for s in cc if isinstance(s,dict) and isinstance(s.get('text'),str)]\n for t in texts:\n i=t.find('[Truncated: PARTIAL view '); j=t.find(EMD,i)\n if i<0 or j<0: continue\n rest=t[j+len(EMD):]\n pb=not rest.startswith('showing') and not rest.startswith('this view is incomplete')\n head = i==0 or t[:i].strip() in ('','<system-reminder>')\n pos[('path' if pb else 'pathless','head' if head else 'embedded',o.get('version'))]+=1\nprint(pos)\nEOF AND csift search 'Truncated: PARTIAL view' -t agent.tool.result --count-by version AND csift search 'Truncated: PARTIAL view' --attachments -t harness.meta.attachment --count-by version AND a python scan of /tmp/cc258.strings for every 'PARTIAL view' construction",
"observed": "BINARY (2.1.258): only two banner constructions exist and BOTH name the path. (1) read time: Ue=!je&&Ne<ke?lfe+`${r}: showing lines 1-${Ne} of ${ke} total ...`:lfe+`${r}: showing the first ...`, carried to the transcript as an attachment {type:\"read_truncation_notice\", banner, toolUseID}. (2) reconstruction time, function qes: lfe+`${d.filePath}: showing ${d.numLines} of ${d.totalLines} lines. Call ${dt} with offset/limit to page through. Do NOT answer from this page alone if the answer may be further in the file.]` with a no-coordinates variant lfe+`${d.filePath}: this view is incomplete and the file cannot be paginated by line. ...]`, guarded by if(typeof _.content===\"string\"&&_.content.startsWith(\"<system-reminder>\"+lfe))return. There is NO pathless construction anywhere in the 2.1.258 binary. CORPUS: 527 tool_result-carried banner occurrences. Splitting by position: 504 are HEAD position (the tool_result content is exactly '<system-reminder>' + banner, i.e. a genuine injected copy) and every single one of those 504 is pathless AND carries a record version <= 2.1.202 (2.1.150 x2, 2.1.156 x12, 2.1.159 x65, 2.1.170 x3, 2.1.177 x279, 2.1.186 x1, 2.1.187 x1, 2.1.191 x73, 2.1.196 x29, 2.1.199 x6, 2.1.200 x8, 2.1.202 x25). ZERO head-position copies exist at any version above 2.1.202, pathless or path-bearing. The remaining 23 pathless + 7 path-bearing occurrences are EMBEDDED mid-content at versions 2.1.179-2.1.258 - Read echoes of files that quote a banner, not injected copies. The reconstruction sentence from qes appears in ZERO corpus records. csift corroborates the carrier migration independently: 'Truncated: PARTIAL view' -t agent.tool.result gives 544 records spanning 2.1.150-2.1.202 plus 19 stragglers at 2.1.231/2.1.258, while -t harness.meta.attachment gives 184 records spanning 2.1.206-2.1.258 (one outlier at 2.1.177). Attachment side: 183 banners, 183 path-bearing, 0 pathless.",
"rule": "One observation per banner occurrence, classified on two axes: carrier (attachment.banner vs a tool_result content string) and position (HEAD = the text before the '[Truncated:' bracket is empty or exactly '<system-reminder>'; EMBEDDED otherwise). Path-bearing = the text after the U+2014 separator does not begin 'showing' or 'this view is incomplete'. Whole corpus.",
"note": "Drift is in the carrier, not just the wording: current Claude Code writes the banner once, as a read_truncation_notice attachment, and writes no tool_result copy. The claim's 528-pathless / 4-path-bearing tool_result split is a whole-corpus HISTORY count, and reading it as a statement about current behavior would send a detector hunting for a copy that is no longer emitted. The claim's warning still has teeth in the opposite direction: a detector written today against the attachment-only, path-bearing form would silently see nothing on any transcript at version <= 2.1.202, where 504 of the corpus's 690 genuine banner emissions live. code-site note: src/recover/carriers.rs 193-204 confirmed verbatim (the PartialRead else-arm). csift parses no banner text on any surface, so this drift changes no csift behavior."
}
]
},
{
"id": "FH-029",
"area": "freshness-file-history",
"behavior": "The line-branch banner states the exact next-page coordinates as `Call Read with offset=${Ne+1} limit=${Ne} for the next page`, i.e. the next window starts one past the last line delivered and requests the same page size.",
"depends": "This makes a capped read's missing range machine-derivable from the banner text alone, which is the cheapest way for csift to disclose the gap in a `recover --coverage` window without reading the file.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "190-196",
"snippet": " source,\n },\n });\n } else {\n events.push(FileEvent {\n line_no,\n turn_index,"
}
],
"instrument": "Read one banner: parse any record under ~/.claude/projects whose `attachment.type` is `read_truncation_notice` and print `attachment.banner`; expect the literal `Call Read with offset=<N+1> limit=<N> for the next page, or Grep to find a specific section.` where N is the `showing lines 1-N` upper bound. Measured now: matches on every one of the 139 line-branch banners.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 -c \"src=open('/tmp/cc258.strings',errors='replace').read();i=src.find('Ue=!je&&Ne<ke');print(repr(src[i:i+330]))\" AND python3 - <<'EOF'\nimport json,os,re,collections\nROOT=os.path.expanduser('~/.claude/projects')\nRE=re.compile(r'showing lines 1-(\\\\d+) of (\\\\d+) total \\\\((\\\\d+) tokens, cap (\\\\d+)\\\\)\\\\. Call (\\\\w+) with offset=(\\\\d+) limit=(\\\\d+) for the next page, or (\\\\w+) to find a specific section\\\\.')\nk=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n for f in fn:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if 'read_truncation_notice' not in line: continue\n try: a=json.loads(line).get('attachment')\n except Exception: continue\n if not (isinstance(a,dict) and a.get('type')=='read_truncation_notice'): continue\n b=a.get('banner') or ''\n m=RE.search(b)\n if m: k['ok' if int(m.group(6))==int(m.group(1))+1 and int(m.group(7))==int(m.group(1)) else 'MISMATCH']+=1\n elif 'cannot be paginated by line' in b: k['char-branch']+=1\n else: k['unmatched']+=1\nprint(k)\nEOF",
"observed": "Binary template verbatim: Ue=!je&&Ne<ke?lfe+`${r}: showing lines 1-${Ne} of ${ke} total (${Ot.tokenCount} tokens, cap ${I}). Call ${dt} with offset=${Ne+1} limit=${Ne} for the next page, or ${Wo} to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]` -- the offset argument is literally the interpolation Ne+1 and the limit argument is literally Ne, where Ne is the same value rendered as the upper bound of 'showing lines 1-${Ne}'. Corpus: of 183 read_truncation_notice attachments, 152 take the line branch and 152 of 152 satisfy offset == N+1 and limit == N; 0 mismatches; the other 31 are the character branch. The historical tool_result copies add 523 line-branch instances, also 523 of 523 conforming. Concrete instance: 'showing lines 1-206 of 255 total (26183 tokens, cap 25000). Call Read with offset=207 limit=206 for the next page, or Grep to find a specific section.'",
"rule": "One observation per read_truncation_notice attachment; a banner counts 'ok' only when the regex-captured offset equals the captured N plus one AND the captured limit equals N. Whole corpus.",
"note": "Holds exactly, at both the template and the instance level, with zero counterexamples in 675 line-branch banners across every version in the corpus. Only the instrument's headline number moved with corpus growth: 152 line-branch attachment banners now, against the 139 the claim recorded. The derivation the depends clause wants is sound - the missing range is (N+1 .. T) read straight off 'showing lines 1-N of T total' - but note the coordinates are only present on the line branch; see FH-030 for the 17% of banners that carry none. code-site note: src/recover/carriers.rs 190-196 confirmed verbatim."
}
]
},
{
"id": "FH-030",
"area": "freshness-file-history",
"behavior": "The character-branch banner is a different sentence - `showing the first <X> of <Y> characters (<T> tokens, cap <C>); this file has very long lines and cannot be paginated by line.` - and reports CHARACTERS, not lines, so it carries no line coordinates at all.",
"depends": "A banner parser that assumes the `showing lines A-B` grammar silently fails on this variant; for csift it is the one case where the capped window cannot be expressed as a line range at all.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "190-196",
"snippet": " source,\n },\n });\n } else {\n events.push(FileEvent {\n line_no,\n turn_index,"
}
],
"instrument": "Corpus: count records whose `attachment.banner` contains `very long lines and cannot be paginated by line`, one observation per record. Measured now: 31 of 170 notice attachments.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 -c \"src=open('/tmp/cc258.strings',errors='replace').read();i=src.find('showing the first');print(repr(src[i-40:i+430]))\" AND the same read_truncation_notice walk as FH-029, counting banners containing 'very long lines and cannot be paginated by line'",
"observed": "Binary template verbatim, as the else-branch of the same ternary: lfe+`${r}: showing the first ${nt.length} of ${me.length} characters (${Ot.tokenCount} tokens, cap ${I}); this file has very long lines and cannot be paginated by line. Use ${Wo} to find a specific section, or ${dt} with offset/limit to page through it. Do NOT answer from this excerpt alone if the answer may be elsewhere in the file.]` -- the quantities interpolated are nt.length and me.length, i.e. string lengths in characters, and no line number appears anywhere in the sentence. Corpus: 31 of 183 read_truncation_notice attachments (17.0%) contain 'very long lines and cannot be paginated by line'; the other 152 are the line branch; 31 + 152 = 183, so the two branches partition the set exactly with no third form. The claim's 31 is unchanged; its denominator has grown from 170 to 183.",
"rule": "One observation per record whose attachment.type is read_truncation_notice, bucketed by whether the banner contains the literal 'very long lines and cannot be paginated by line'. Whole corpus.",
"note": "Holds, and the branch selector is worth recording because it is not what the sentence implies: the ternary is Ue=!je&&Ne<ke?line:char, where je is set when the cut landed mid-line, so the character branch is chosen by the CUT falling inside a line, not by any property of the file measured up front. The reconstruction-time banner found under FH-028 has its own coordinate-free variant ('this view is incomplete and the file cannot be paginated by line'), so a parser guarding against the line grammar must tolerate two coordinate-free sentences, not one. code-site note: src/recover/carriers.rs 190-196 confirmed verbatim."
}
]
},
{
"id": "FH-031",
"area": "freshness-file-history",
"behavior": "A Write carrier that CREATED a file is persisted as {type:\"create\", filePath, content, structuredPatch:[], originalFile:null, userModified} plus up to two CONDITIONALLY SPREAD optional keys - memdirStamped (58 of 2,598 creates) and gitDiff (schema-declared gitDiff:kQe().optional(), 0 occurrences corpus-wide). originalFile is null by construction on a create, and on an update it is null when \"the previous content was too large to include\" - measured at 65 of 291 updates (22.3%).",
"depends": "csift uses an Edit's `originalFile` versus the replayed buffer as an AUTHORITATIVE drift boundary, so a null `originalFile` disarms that cross-check for that op; the majority of real Edits are in exactly that state, which is why the boundary set is explicitly an honestly-bounded detectable subset rather than a guarantee.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "91-93",
"snippet": " // ── (2) Write result: {type:create|update, filePath, content, …} ──\n // ── (3) Edit result: {filePath, oldString, newString, structuredPatch, …} (no type) ──\n let path = tur.get(\"filePath\").and_then(serde_json::Value::as_str);"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 executable> | grep -o 'originalFile:i().nullable().describe(\"The original file content before the write[^\"]*\"' - expect the describe string naming both null cases. Counting rule: one hit per schema literal.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "SPEC.md section 6.7 extraction table; schema description measured 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import re;src=open('/tmp/cc258.strings',errors='replace').read();[print(repr(m.group(0))) for m in re.finditer(r'originalFile:[A-Za-z_$]*\\\\(\\\\)\\\\.nullable\\\\(\\\\)\\\\.describe\\\\(\\\"[^\\\"]*\\\"',src)]; i=src.find('let ke={type:\\\"create\\\"');print(repr(src[i:i+230]))\" AND python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects')\nk=collections.Counter(); keys=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n for f in fn:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if '\\\"filePath\\\"' not in line or 'toolUseResult' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if not (isinstance(t,dict) and isinstance(t.get('filePath'),str)): continue\n ty=t.get('type')\n if ty in ('create','update'):\n keys[(ty,tuple(sorted(t.keys())))]+=1\n k[(ty,'origNull' if t.get('originalFile') is None else 'origStr')]+=1\n if ty=='create': k[('create','sp_empty' if t.get('structuredPatch')==[] else 'sp_other')]+=1\nprint(k); print(keys)\nEOF",
"observed": "Binary schema verbatim: originalFile:i().nullable().describe(\"The original file content before the write (null for new files, or when the previous content was too large to include)\") -- both null cases named exactly as the claim quotes. The create construction is also literal in the binary: let ke={type:\"create\",filePath:e,content:n,structuredPatch:[],originalFile:null,userModified:v??!1,...z&&{memdirStamped:!0},..._e&&{gitDiff:_e}}, and its update sibling: Ie={type:\"update\",filePath:e,content:n,structuredPatch:we,originalFile:fe?null:W,userModified:v??!1,...z&&{memdirStamped:!0},..._e&&{gitDiff:_e}} -- so originalFile:null is a compile-time constant on create, not a runtime outcome. Corpus: 2,889 Write carriers total. Creates 2,598: originalFile null on 2,598 of 2,598 (100%), structuredPatch [] on 2,598 of 2,598, userModified false on 2,598 of 2,598. Updates 291: originalFile null on 65 (22.3%), a string on 226. Key sets: creates are ('content','filePath','originalFile','structuredPatch','type','userModified') x2,540 plus a seven-key variant adding 'memdirStamped' x58; updates are the same six-key set x290 plus one memdirStamped variant. 'gitDiff' occurs 0 times on any Write carrier corpus-wide despite being schema-declared.",
"rule": "One observation per record whose toolUseResult is a dict with a string filePath and type in {create, update}; tallied by type, by whether originalFile is JSON null, and by the sorted tuple of the carrier's own keys. Whole corpus.",
"note": "The substance holds without exception: originalFile is a literal null in the create construction and is null on 100% of the 2,598 real creates. Two refinements. First, the key enumeration is a base, not a closed set - two keys are spread in conditionally, so a consumer must not treat an unexpected key as a malformed carrier. Second, the update rate is worth pinning at 22.3%: 65 of 291 updates carry a null originalFile, which is where the depends clause's disarmed cross-check actually bites on the Write side. code-site note: src/recover/carriers.rs 91-93 confirmed verbatim."
}
]
},
{
"id": "FH-032",
"area": "freshness-file-history",
"behavior": "An Edit carrier is persisted as {filePath, oldString, newString, originalFile (nullable), structuredPatch, userModified, replaceAll} plus up to three CONDITIONALLY SPREAD optional keys - staleRecovered (92 corpus-wide), memdirStamped (43) and gitDiff (schema-declared, 0 occurrences corpus-wide) - and carries NO type field, that absence being what distinguishes it from a Write carrier. Its originalFile, schema-declared .nullable() (\"The original file contents before editing\"), is NULL on the MAJORITY of real edits: 9,690 null against 3,682 present over 13,372 Edit carriers corpus-wide, and 8,444 null against 2,977 string over the 11,434 echoes of a 120-transcript window.",
"depends": "csift discriminates the two carrier shapes by the presence of `oldString`/`newString` rather than by `type`, then keeps the strings plus `structuredPatch` plus `originalFile`; its AUTHORITATIVE `original_file_disagreement` boundary can therefore fire on only about one edit in four, so a clean `recover --coverage` is evidence over that fraction and the skipped check is never reported as skipped. A `type` field appearing on Edit carriers would flip every edit into the Write full-snapshot arm and wipe the buffer at each one.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "97-98",
"snippet": " let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));"
},
{
"path": "src/recover/replay.rs",
"lines": "131-136",
"snippet": " // Boundary cross-check: originalFile vs replayed buffer.\n if let Some(orig) = original_file {\n if had_full_anchor && buffer_disagrees_with_original(&buf, orig) {\n out.boundaries.push(Boundary {\n line_no: e.line_no,\n turn_index: e.turn_index,"
},
{
"path": "src/recover/replay.rs",
"lines": "531-535",
"snippet": "pub(crate) fn buffer_disagrees_with_original(buf: &SparseBuffer, original_file: &str) -> bool {\n let orig_lines = split_lines(original_file);\n if orig_lines.is_empty() {\n return false;\n }"
}
],
"instrument": "Whole corpus: for every dict `toolUseResult` with a `filePath` and no `create`/`update` `type`, tally whether `originalFile` is null, whether `structuredPatch` is truthy and whether `replaceAll` is truthy - counting rule one count per carrier RECORD; measured 2026-09-02: 13,372 with a structuredPatch, 9,690 null originalFile against 3,682 present, 70 with `replaceAll:true`. Narrower window: the same tally over the 120 most-recently-modified top-level transcripts, one tally per Edit echo - measured 2026-09-02: null 8,443, str 2,975 (11,418 total; the live window drifts by single digits between runs). Schema: `strings -a <the Claude Code 2.1.258 executable> | grep -F 'originalFile:i().nullable().describe(\"The original file contents before editing\")' | wc -l` expects 1.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.7 extraction table; census measured 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -a -n 6 ~/.local/share/claude/versions/2.1.258 | grep -F 'originalFile:i().nullable().describe(\"The original file contents before editing\")' | wc -l AND python3 -c \"src=open('/tmp/cc258.strings',errors='replace').read();i=src.find('return{data:{filePath:F,oldString');print(repr(src[i:i+230]))\" AND python3 - <<'EOF'\nimport json,os,glob,collections\nROOT=os.path.expanduser('~/.claude/projects')\nk=collections.Counter(); keys=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n for f in fn:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if '\\\"filePath\\\"' not in line or 'toolUseResult' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if not (isinstance(t,dict) and isinstance(t.get('filePath'),str)): continue\n if t.get('type') in ('create','update'): continue\n keys[tuple(sorted(t.keys()))]+=1\n k['type_key_present=%s'%('type' in t)]+=1\n of=t.get('originalFile','MISSING')\n k['null' if of is None else ('missing' if of=='MISSING' else 'str')]+=1\n if t.get('structuredPatch'): k['structuredPatch']+=1\n if t.get('replaceAll'): k['replaceAll']+=1\nprint(k); print(keys)\nEOF (and the same tally restricted to the 120 most-recently-modified files matched by ~/.claude/projects/*/*.jsonl)",
"observed": "Schema literal count = 1, as the claim expects. The Edit construction is literal in the binary and carries NO type key: return{data:{filePath:F,oldString:me,newString:j,originalFile:fe,structuredPatch:ke,userModified:_??!1,replaceAll:B,...we&&{staleRecovered:!0},...Ie&&{memdirStamped:!0},...De&&{gitDiff:De}}} -- so the discriminator is structural, not statistical. Corpus, matching the claim's counting rule exactly: 13,385 carriers with a string filePath and no create/update type; 'type' key present on 0 of 13,385; 13,372 with a truthy structuredPatch; originalFile null 9,690 against a string 3,682 (both the claim's numbers, to the unit); replaceAll true on 70. Key sets: ('filePath','newString','oldString','originalFile','replaceAll','structuredPatch','userModified') x13,237, the same plus 'staleRecovered' x92, the same plus 'memdirStamped' x43, and 13 residue carriers that are not Edits at all - ('filePath','hasTaskTool','isAgent','plan') x8 and the same plus 'planWasEdited' x5. 'gitDiff' occurs 0 times across all 13,372 Edit carriers. Narrower window, the 120 most-recently-modified top-level transcripts: null 8,444, string 2,977 (claim: 8,443 and 2,975 - a drift of 1 and 2, inside the claim's own stated single-digit tolerance). Ratio: 3,682/13,372 = 27.5%, i.e. the claim's \"about one edit in four\".",
"rule": "One count per carrier RECORD whose toolUseResult is a dict with a string filePath and whose type is neither create nor update; tallied by originalFile nullity, by truthy structuredPatch, by truthy replaceAll, and by the sorted key tuple. Whole corpus, then the same tally over the 120 most-recently-modified files matching ~/.claude/projects/*/*.jsonl.",
"note": "Every number in the claim reproduces to the unit on the whole corpus, and the window numbers moved by 1 and 2 as the claim itself predicted. Two refinements. gitDiff is real in the schema but has never fired here, so it is a documented possibility rather than an observed field - and a further optional, memdirStamped, was missing from the enumeration. The type-absence discriminator is now confirmed structurally rather than by census: the Edit result object is built with no type key at all, so the claim's worry (a type field appearing on Edit carriers and flipping every edit into the Write arm) would require a change to that construction, not a data accident. csift's own discriminator reads oldString/newString, which is the same structural fact from the other side. code-site note: All three sites confirmed verbatim: src/recover/carriers.rs 97-98, src/recover/replay.rs 131-136, src/recover/replay.rs 529-533."
}
]
},
{
"id": "FH-033",
"area": "freshness-file-history",
"behavior": "The `edited_text_file` attachment's payload keys are exactly `type`, `filename` and `snippet` - never `filePath` or `content` - and an EMPTY `snippet` string is a real, emitted form.",
"depends": "csift's `filename`-then-`filePath` and `snippet`-then-`content` fallbacks are dead alternatives on current Claude Code, and the empty-snippet case is the degraded external-edit boundary csift already names in the boundary detail.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "219-224",
"snippet": " if atype == Some(\"edited_text_file\") {\n let path = att\n .get(\"filename\")\n .or_else(|| att.get(\"filePath\"))\n .and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {"
}
],
"instrument": "Corpus: count `tuple(sorted(attachment.keys()))` for every record whose `attachment.type` is `edited_text_file`, one observation per record. Measured now over all of ~/.claude/projects: 1339 records, all with the key set `('filename','snippet','type')`; 85 of the 1339 carry an empty `snippet` (6.3%).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects')\nks=collections.Counter(); e=collections.Counter(); byver=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n for f in fn:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if 'edited_text_file' not in line: continue\n try: o=json.loads(line)\n except Exception: continue\n a=o.get('attachment')\n if isinstance(a,dict) and a.get('type')=='edited_text_file':\n ks[tuple(sorted(a.keys()))]+=1\n e['empty' if a.get('snippet')=='' else 'nonempty']+=1\n if a.get('snippet')=='': byver[o.get('version')]+=1\nprint(ks); print(e); print(byver)\nEOF AND python3 -c \"import re;src=open('/tmp/cc258.strings',errors='replace').read();[print(repr(src[max(0,m.start()-160):m.start()+120])) for m in re.finditer(r'type:\\\"edited_text_file\\\"',src)]; i=src.find('if(_.type!==\\\"edited_text_file\\\")continue');print(repr(src[i-60:i+170])); print(re.search(r'vQo=[0-9]+',src).group(0))\"",
"observed": "Corpus: 1,339 records whose attachment.type is edited_text_file; the key set is ('filename','snippet','type') on 1,339 of 1,339 - a single key set, with no filePath and no content anywhere. 85 of the 1,339 carry an empty snippet (6.3%), and those empties are spread across 12 Claude Code versions from 2.1.177 to 2.1.258, including 2 records at 2.1.258 itself, so the empty form is current, not a fossil. Binary: the construction is return{type:\"edited_text_file\",filename:C,snippet:j}, three keys, and it is guarded by if(j===\"\")return null - yet the empty form still ships, because a LATER pass blanks it: let f=0;for(let _ of d){if(_.type!==\"edited_text_file\")continue;if(f>=vQo)_.snippet=\"\";else f+=_.snippet.length} with vQo=16384. Attachments are walked in order, their snippet lengths accumulated, and every edited_text_file past a cumulative 16,384 characters has its snippet overwritten with the empty string.",
"rule": "One observation per record whose attachment.type is edited_text_file, keyed by the sorted tuple of the attachment's own keys, then bucketed by whether snippet is the empty string, then by the record's Claude Code version. Whole corpus.",
"note": "Holds on every count, to the unit (1,339 and 85), and the empty form is confirmed live at 2.1.258 rather than inferred. The mechanism is now pinned and is worth recording because it is not a per-snippet cap: the budget vQo=16384 is CUMULATIVE across the attachments of one batch, applied after construction, so whether a given external edit arrives with content depends on how many other edited files preceded it in the same batch - not on that file's own size. The claim's depends clause is confirmed from the other side too: the construction emits exactly three keys, so csift's filePath and content fallbacks can never be reached on current Claude Code. code-site note: src/recover/carriers.rs 219-224 confirmed verbatim."
}
]
},
{
"id": "FH-034",
"area": "freshness-file-history",
"behavior": "The 8,192-character cap applies to the diff BODY before the tail is appended, and the cut backs up to the last newline at or before 8,192 (falling back to a hard 8,192 slice when there is no earlier newline). So the EMITTED snippet is body + `\\n... [N lines truncated] ...` and can exceed 8,192 - 8,223 characters observed - while a truncated snippet can also be far shorter than the cap, 597 characters observed, when the body's last newline before position 8,192 sits early because of one very long line.",
"depends": "csift treats the stripped snippet as observed content for the external-edit boundary detail; the 8192-char cap means half of all real snippets are themselves truncated, so the snippet is never a complete picture of the external change.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "225-230",
"snippet": " let snippet_text = att\n .get(\"snippet\")\n .or_else(|| att.get(\"content\"))\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default();\n let snippet = strip_gutter(snippet_text);"
}
],
"instrument": "Seek `function EJt(` in a `strings -a` dump of the 2.1.258 binary (`context:8`, the `TQe` cap, and the `... [${F} lines truncated] ...` tail are one function), then `grep -o 'TQe=[0-9]*'` for the cap - expect `TQe=8192`. Corpus: count `edited_text_file` snippets containing `lines truncated] ...`, one observation per attachment - measured now: 651 of 1254 non-empty snippets (52%).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import re;src=open('/tmp/cc258.strings',errors='replace').read();i=src.find('function EJt(');print(repr(src[i:i+470]));j=src.find('function j8t(');print(repr(src[j:j+330]));print(re.search(r'TQe=[0-9]+',src).group(0))\" AND python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects')\nk=collections.Counter(); mx=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n for f in fn:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if 'edited_text_file' not in line: continue\n try: a=json.loads(line).get('attachment')\n except Exception: continue\n if not (isinstance(a,dict) and a.get('type')=='edited_text_file'): continue\n s=a.get('snippet')\n if not isinstance(s,str) or s=='': continue\n t='lines truncated] ...' in s\n k['trunc' if t else 'whole']+=1\n mx['max_trunc' if t else 'max_whole']=max(mx['max_trunc' if t else 'max_whole'],len(s))\n if not t and len(s)>8192: k['whole_over_cap']+=1\nprint(k); print(mx)\nEOF",
"observed": "Binary, one function, verbatim: function EJt(e,n){let r=rne(\"file.txt\",\"file.txt\",e,n,void 0,void 0,{context:8,timeout:qCe});if(!r)return\"\";let o=iz(),d=r.hunks.map((B)=>({startLine:B.oldStart,content:B.lines.filter((U)=>!U.startsWith(\"-\")&&!U.startsWith(\"\\\\\")).map((U)=>U.slice(1)).join(`\\n`),tabAwareSeparator:o})).map(j8t).join(`\\n`);if(d.length<=TQe)return d;let f=d.lastIndexOf(`\\n`,TQe),_=f>0?d.slice(0,f):d.slice(0,TQe),v=1,C=1,F=_n(d,`\\n`,_.length+v)+C;return`${_}\\n... [${F} lines truncated] ...`} -- every element the claim names is present: context:8, the removal and '\\\\ No newline' filter, the .slice(1) marker-column strip, and the tail. TQe=8192. The gutter renderer is function j8t({content:e,startLine:r,tabAwareSeparator:n=!1}) which walks lines emitting `${o}${sep}${text}` from o=startLine, so the numbering does start at the hunk's oldStart. Corpus: 1,254 non-empty snippets, 651 carry 'lines truncated] ...' (51.9%). Longest snippet WITHOUT the tail: 8,186 characters, and 0 of the 603 untruncated snippets exceed 8,192 - so the cap is real and never violated on the body. Longest snippet overall: 8,223 characters, i.e. 31 over the cap, which is the appended tail. Truncated snippets range 597 to 8,223 characters.",
"rule": "One observation per record whose attachment.type is edited_text_file with a non-empty snippet, bucketed by whether the snippet contains the literal 'lines truncated] ...', with the maximum character length tracked per bucket. Whole corpus.",
"note": "Every structural element of the claim is confirmed in a single binary function, and the headline 651 of 1,254 (52%) reproduces to the unit. The refinement matters for a consumer that would size a buffer or test length<=8192 to decide truncation: neither works. The reliable truncation test is the literal tail, and the reliable statement about the cap is that it bounds the body, not the field. One consequence for the depends clause: because the cut is at a newline before the cap rather than at the cap, the shortfall is unbounded - a snippet can stop at 597 characters and still claim to represent an external change, so the snippet is not merely incomplete but incomplete by an amount the record does not disclose except as a line count in the tail. code-site note: src/recover/carriers.rs 225-230 confirmed verbatim."
}
]
},
{
"id": "FH-035",
"area": "freshness-file-history",
"behavior": "`staleRecovered:true` rides on the EDIT echo only (never the Write or Read shape) and means Claude Code found the file changed on disk since the last Read yet applied the edit because `oldString` stayed unique.",
"depends": "csift reads the flag INSIDE the Edit arm, which returns before the Write full-content arm, and turns it into the non-invalidating `stale_recovered` annotation boundary; if the flag ever moved to the Write shape it would be read by no arm at all.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "131-138",
"snippet": " // `staleRecovered:true` on a SUCCESSFUL Edit: CC found the file modified on\n // disk since the last read, but old_string stayed unique so the edit applied.\n // The disk holds changes this stream never saw - an authoritative annotation.\n if tur\n .get(\"staleRecovered\")\n .and_then(serde_json::Value::as_bool)\n .unwrap_or(false)\n {"
},
{
"path": "src/recover/carriers.rs",
"lines": "87-151",
"snippet": " return;\n }\n\n // A Write result: full-content anchor.\n if let Some(content) = tur.get(\"content\").and_then(serde_json::Value::as_str) {\n let total = line_count(content);"
},
{
"path": "src/recover/types.rs",
"lines": "62-67",
"snippet": " /// A SUCCESSFUL Edit whose `toolUseResult.staleRecovered:true` reports the file\n /// had been modified on disk since the last read; the edit still applied cleanly\n /// (old_string stayed unique). The buffer's edited span is right, but the disk\n /// holds other changes this stream never saw: an authoritative, NON-invalidating\n /// annotation boundary.\n StaleRecovered,"
}
],
"instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if 'staleRecovered' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if isinstance(t,dict) and 'staleRecovered' in t:\n k['edit' if 'oldString' in t else ('write' if 'content' in t else 'other')]+=1\nprint(k)\nEOF\nCounting rule: one tally per echo carrying a `staleRecovered` key, split by shape. Measured 2026-09-02: edit 80, write 0, other 0. Earliest Claude Code version in the corpus carrying the key: 2.1.207.",
"located": {
"claude_code": "2.1.233",
"csift": "0.8.0",
"source": "AGENTS.md section 3.11; carrier-shape census measured 2026-09-02"
},
"first_seen_claude_code": "2.1.207",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if 'staleRecovered' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if isinstance(t,dict) and 'staleRecovered' in t:\n k['edit' if 'oldString' in t else ('write' if 'content' in t else 'other')]+=1\nprint(k)\nEOF (and the same walk over the whole corpus via os.walk, additionally tallying the value and the record version) AND python3 -c \"import re;src=open('/tmp/cc258.strings',errors='replace').read();print(src.count('staleRecovered'));[print(repr(src[max(0,m.start()-150):m.start()+120])) for m in re.finditer('staleRecovered',src)]\"",
"observed": "Whole corpus: 92 echoes carry a staleRecovered key; split by shape, edit 92, write 0, other 0; the value is true on 92 of 92 and false on 0. The 120-transcript window reproduces the claim's figure exactly: edit 80, write 0, other 0. Versions carrying the key run 2.1.207 (earliest, 4 echoes) through 2.1.258 (1 echo), so the flag is both first-seen where the claim says and still live on the current build. Binary: 'staleRecovered' occurs 5 times and every occurrence is inside the Edit tool - the result construction return{data:{filePath:F,oldString:me,newString:j,originalFile:fe,structuredPatch:ke,userModified:_??!1,replaceAll:B,...we&&{staleRecovered:!0},...}}, the Edit tool's mapToolResultToToolResultBlockParam destructure, and two sites in the edit helper that produces it. Neither Write construction (type:\"create\" or type:\"update\") mentions it.",
"rule": "One tally per echo carrying a staleRecovered key, split by carrier shape - 'edit' when oldString is present, else 'write' when content is present, else 'other' - then by the key's JSON value and the record's Claude Code version. Run over the whole corpus and over the 120 most-recently-modified files matching ~/.claude/projects/*/*.jsonl.",
"note": "Holds on both instruments, and the census result is now backed by a structural reason rather than a sample: the key is spread into the Edit result object only, and the string does not appear in either Write construction, so the claim's failure mode (the flag moving to the Write shape, where csift's Edit-arm read would never see it) cannot happen by data variation - it would take a change to that construction. Worth noting alongside FH-027: staleRecovered uses the same conditional-spread idiom, so it too is never written false, and csift's .and_then(as_bool).unwrap_or(false) reads absence correctly. code-site note: All three sites confirmed verbatim: src/recover/carriers.rs 131-138, src/recover/carriers.rs 146-151, src/recover/types.rs 62-67."
}
]
},
{
"id": "FH-036",
"area": "freshness-file-history",
"behavior": "When an Edit applies to a file that changed on disk since the last Read, Claude Code both sets `toolUseResult.staleRecovered` and appends a prose note to the tool_result string (` (note: the file had been modified on disk since you last read it — the edit applied cleanly, but the file contains other changes not in your context. Read it before edits that depend on surrounding content.)`).",
"depends": "csift reads ONLY the structured flag to raise its `stale_recovered` annotation boundary, so in a carrier-less workflow lane - where the flag does not exist and only the prose note survives - that authoritative freshness signal is lost and `recover --coverage` reports one fewer soft boundary than really occurred.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "131-135",
"snippet": " // `staleRecovered:true` on a SUCCESSFUL Edit: CC found the file modified on\n // disk since the last read, but old_string stayed unique so the edit applied.\n // The disk holds changes this stream never saw - an authoritative annotation.\n if tur\n .get(\"staleRecovered\")"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 executable> | grep -o 'staleRecovered:f[^`]\\{0,260\\}' for the note literal; python3 over ~/.claude/projects counting Edit carriers (a dict `toolUseResult` carrying `oldString`) whose `staleRecovered` is truthy. Counting rule: one count per carrier RECORD. Observed 92 out of 13372 Edit carriers.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "AGENTS.md section 3.11 and SPEC.md section 6.7 extraction table; note literal and counts measured 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a <the Claude Code 2.1.258 executable> | grep -o 'the file had been modified on disk since you last read it[^\"`]\\{0,220\\}' and grep -o '.\\{300\\}the file had been modified on disk since you last read it.\\{0,240\\}'; then python3 walking ~/.claude/projects, byte-prefiltering each jsonl line on b'oldString', parsing the survivors, counting records whose toolUseResult is a dict containing 'oldString' (Edit carriers) and, among those, the ones whose toolUseResult.staleRecovered is truthy, plus whether the rendered tool_result text of each such record contains the prose note.",
"observed": "Binary: `mapToolResultToToolResultBlockParam(e,n){let{filePath:r,userModified:o,replaceAll:d,staleRecovered:f,memdirStamped:_}=e,v=o?\". The user modified your proposed changes before accepting them. \":\"\",C=f?\" (note: the file had been modified on disk since you last read it \\u2014 the edit applied cleanly, but the file contains other changes not in your context. Read it before edits that depend on surrounding content.)\":o||_?\"\":O3t;` - the structured flag `staleRecovered` and the prose note are produced by the same expression. Corpus: 13372 Edit carriers, 92 with staleRecovered truthy, and 92 of those 92 also carry the prose note in the rendered tool_result text. 7819 jsonl files walked, 0 unparseable candidate lines. csift code site src/recover/carriers.rs:131-135 matches the claimed snippet verbatim (comment lines 131-133, `if tur` 134, `.get(\"staleRecovered\")` 135).",
"rule": "One count per carrier RECORD: a jsonl record whose top-level `toolUseResult` is a JSON object containing the key `oldString` is one Edit carrier; it is a stale carrier iff `toolUseResult.staleRecovered` is truthy. Prose-note pairing counts a stale carrier as paired iff the string `the file had been modified on disk since you last read it` occurs in the concatenation of message.content (string form) and every tool_result block's content (string or [{text}] form).",
"note": "Both halves confirmed on the current binary and the current corpus, and the claim's numbers reproduce exactly (92 of 13372). Added observation the claim did not state: the structured flag and the prose note are not merely correlated, they are emitted by one ternary in the Edit tool's result mapper, and all 92 stale carriers in the corpus carry both. That strengthens the claim's `depends`: the prose note is the only survivor in a carrier-less lane precisely because the flag lives on toolUseResult while the note is spliced into the tool_result string."
}
]
},
{
"id": "FH-037",
"area": "freshness-file-history",
"behavior": "Claude Code's file-mutating structured tool set is exactly the four-element array `[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]`, wrapped in a `Set` and consulted by the predicate that decides whether a `PostToolUse` / `PostToolUseFailure` / `PostToolBatch` event touched a file (the test also resolves the name through an alias expansion, so an MCP tool aliased onto one of the four counts).",
"depends": "csift's `FileOp` enum recognises exactly the same four structured tools as authoritative mutations, so `files --by-file` create-vs-edit accounting and `recover`'s replay stay aligned with the harness's own definition; a fifth mutating tool added to that array would produce mutations csift silently omits.",
"code": [
{
"path": "src/model/mutation.rs",
"lines": "146-152",
"snippet": " let (op, key) = match name {\n \"Write\" => (FileOp::Write, \"file_path\"),\n \"Edit\" => (FileOp::Edit, \"file_path\"),\n \"MultiEdit\" => (FileOp::MultiEdit, \"file_path\"),\n \"NotebookEdit\" => (FileOp::NotebookEdit, \"notebook_path\"),\n _ => continue,\n };"
}
],
"instrument": "`strings -n 30 <the Claude Code 2.1.258 executable> | grep -oE '.{160}\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\].{80}'` -> one hit showing `var opr=[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"],LPt=new Set(opr);function cGe(e,n){...return LPt.has(r)||...}`, and `grep -oE 'function FPt\\(e,n\\)\\{.{0,420}'` -> `switch(e.hook_event_name){case\"PostToolUse\":case\"PostToolUseFailure\":return cGe(e.tool_name,n);case\"PostToolBatch\":return (e.tool_calls??[]).some(...)}`. Counting rule: exact array literal match, one occurrence.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (tool-registry probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a <the Claude Code 2.1.258 executable> > dump; grep -oE '.{170}\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\].{110}' dump; grep -o '\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\]' dump | wc -l; grep -oE 'function cGe\\(e,n\\)\\{.{0,300}' dump; grep -oE 'function FPt\\(e,n\\)\\{.{0,420}' dump; python3 regex over the dump for 'function T5\\(.{0,420}' and for the call sites of FPt(.",
"observed": "Exactly 1 occurrence of the array literal: `var opr=[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"],LPt=new Set(opr);function cGe(e,n){if(e===void 0)return!1;let r=bu(e);return LPt.has(r)||T5(r,n).some((o)=>LPt.has(bu(o)))}function FPt(e,n){switch(e.hook_event_name){case\"PostToolUse\":case\"PostToolUseFailure\":return cGe(e.tool_name,n);case\"PostToolBatch\":return(e.tool_calls??[]).some((r)=>cGe(r.tool_name,n));default:return!1}}`. Alias expansion: `function T5(e,r){if(!r)return[];let t=[];for(let[n,s]of Object.entries(r))if(s===e)t.push(n);return t}` - a reverse lookup over an alias map, so any alias key whose value is one of the four passes. Name normaliser: `function bu(e){return Object.hasOwn(i,e)?i[e]:e}`. Observed consumer: `if(FPt(z,e.toolAliases()))return _(I,\"after_edit_held\"),{}` - the second argument is the live tool-alias map. csift code site src/model/mutation.rs:146-152 matches the claimed snippet verbatim.",
"rule": "Exact array-literal byte match over the whole strings dump, counted with `grep -o ... | wc -l`; one occurrence expected. Predicate shape read verbatim from the bytes immediately following the literal.",
"note": "Confirmed verbatim, including the alias-expansion clause. Two additions a rerunner should know: (1) the observed consumer of FPt is a remote device-hook gate that holds the callback after an edit (`after_edit_held`), which is what makes the four-element set load-bearing rather than decorative; (2) the bundle contains a SECOND, unrelated function also minified to `FPt` (an away-summary helper, `function FPt(e,n){if(!n.onMetadataChanged)return;...}`), so a bare `function FPt(` grep returns two hits - match on the `switch(e.hook_event_name)` body to get the right one."
}
]
},
{
"id": "FH-038",
"area": "freshness-file-history",
"behavior": "Claude Code carries a SECOND, narrower three-element tool list `[\"Edit\",\"Write\",\"NotebookEdit\"]` (`var Ufo`, membership via `function fot(e){return Ufo.includes(e)}`) alongside the four-element mutating set. It is NOT a second definition of \"mutating tool\": both of its call sites are permission-decision telemetry, gating whether a `tool_decision` metric gets an extra `language` dimension resolved from the tool call's path. No freshness, snapshot, or hook-payload signal is governed by it.",
"depends": "The three-element list governs only a telemetry dimension, so it cannot make csift's four-element `FileOp` over-read any consumed signal. The claim's hazard is not reachable through this list; the real exposure to a fifth mutating tool remains the four-element `opr` set covered by FH-037.",
"code": [
{
"path": "src/model/mutation.rs",
"lines": "16-24",
"snippet": "pub enum FileOp {\n /// `Write` tool - writes a file whole (a create when the path was new).\n Write,\n /// `Edit` tool - a single in-place string replacement in an existing file.\n Edit,\n /// `NotebookEdit` tool - edits a Jupyter notebook cell (`notebook_path`).\n NotebookEdit,\n /// `MultiEdit` tool - multiple edits to one file in a single call.\n MultiEdit,"
}
],
"instrument": "`strings -n 30 <the Claude Code 2.1.258 executable> | grep -oE '.{100}\\[\"Edit\",\"Write\",\"NotebookEdit\"\\].{60}'` -> `var Ufo=[\"Edit\",\"Write\",\"NotebookEdit\"];function fot(e){return Ufo.includes(e)}`. Counting rule: exact array literal, one occurrence, distinct from the four-element `opr`.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (tool-registry probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -a <the Claude Code 2.1.258 executable> > dump; grep -oE '.{110}\\[\"Edit\",\"Write\",\"NotebookEdit\"\\].{80}' dump; grep -o '\\[\"Edit\",\"Write\",\"NotebookEdit\"\\]' dump | wc -l; grep -oE 'function fot\\(e\\)\\{return Ufo.includes\\(e\\)\\}.{0,700}' dump; grep -o 'fot(' dump | wc -l; grep -oE '.{200}[^a-zA-Z]fot\\([a-zA-Z_$.]{1,20}\\).{0,160}' dump.",
"observed": "Exactly 1 occurrence of the three-element literal: `var Ufo=[\"Edit\",\"Write\",\"NotebookEdit\"];function fot(e){return Ufo.includes(e)}`, distinct from the four-element `opr`. Its two membership call sites are both permission-decision telemetry: `if(fot(o.name))pot(o,d,I,j).then((z)=>Gkt()?.add(1,z))` and `if(fot(e.name))pot(e,Ie,xn,An).then((Cn)=>Gkt()?.add(1,Cn))`. The consumer is `async function pot(e,n,r,o){let d;if(e.getPath&&n){let f=e.inputSchema.safeParse(n);if(f.success){let _=e.getPath(f.data);if(_)d=await gDe(_)}}return{decision:r,source:o,tool_name:e.name,...d&&{language:d}}}` - it resolves the tool call's path and attaches a detected `language` to a metric counter. `grep -o 'fot(' | wc -l` = 6, of which 2 are these membership tests, 1 is the definition, and 3 belong to an unrelated same-named helper. csift code site src/model/mutation.rs:16-24 matches the claimed snippet verbatim.",
"rule": "Exact array-literal byte match over the strings dump (one occurrence); then every `fot(` occurrence read in context and classified by its enclosing expression as definition / membership test / unrelated homonym.",
"note": "The literal fact (a second, narrower array exists, one occurrence, distinct from the four-element one) survives refutation exactly as written. What needed correction is the framing and therefore the hazard: reading the call sites shows the three-element list is a telemetry subset, not a rival definition of the mutating-tool set, so the claim's `depends` (\"csift's four-element assumption over-reads it\") does not hold. A rerunner should classify each `fot(` occurrence in context - the bundle reuses both `fot` and `pot` for unrelated helpers (`function fot(e){return e?180000:60000}`), so symbol name alone is not evidence."
}
]
},
{
"id": "FH-039",
"area": "freshness-file-history",
"behavior": "Claude Code rewrites its own settings in-process on model, config, theme, permission and plugin changes with NO tool record, and the settings paths it knows about are broader than two: the binary carries `.claude/settings.json` (145 string-table occurrences), `.claude/settings.local.json` (88), `managed-settings.json` (36) and `.claude.json` (8).",
"depends": "csift's settings-family detector matches exactly a basename of `settings.json` or `settings.local.json` whose immediate parent directory is `.claude`, so a version bump on a managed-settings file, on the per-project state file, or on an MCP/plugin config is reported as an ORDINARY external write and the replay's settings-mutation annotation is not applied.",
"code": [
{
"path": "src/files/external.rs",
"lines": "10-14",
"snippet": "//! SCOPE LAW (operator-ruled): reported ONLY for the settings family - basename\n//! `settings.json` / `settings.local.json` under a `.claude` parent. The tracked set\n//! spans thousands of ordinary source paths and CC writes bookkeeping constantly;\n//! listing \"harness writes\" beyond settings would flood every timeline (measured\n//! corpus: 1701 tracked paths, 11 settings-family)."
},
{
"path": "src/files/external.rs",
"lines": "25-32",
"snippet": "/// True for the settings family: `settings.json` / `settings.local.json` directly\n/// under a `.claude` directory (absolute or relative spelling alike).\npub(crate) fn is_settings_family(path: &str) -> bool {\n let mut parts = path.rsplit(['/', '\\\\']);\n let base = parts.next().unwrap_or_default();\n let parent = parts.next().unwrap_or_default();\n matches!(base, \"settings.json\" | \"settings.local.json\") && parent == \".claude\"\n}"
}
],
"instrument": "`strings -n 8 <the Claude Code 2.1.258 executable> | grep -oE '\\.claude/settings[a-z.]*json|managed-settings\\.json|\"\\.claude\\.json\"' | sort | uniq -c`. Counting rule: one per string-table occurrence; compare the resulting path set against `is_settings_family`'s two accepted basenames.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.4",
"source": "measured 2026-09-02 (settings-family probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a <the Claude Code 2.1.258 executable> | grep -oE '\\.claude/settings[a-z.]*json|managed-settings\\.json|\"\\.claude\\.json\"' | sort | uniq -c | sort -rn; then `csift files . --by timeline | rg 'external write'` in the csift repo; then python3 walking ~/.claude/projects, byte-prefiltering on b'file-history-snapshot', collecting every tracked path key from file-history-snapshot / file-history-delta records and classifying each basename by csift's is_settings_family rule.",
"observed": "String-table census: 145 `.claude/settings.json`, 88 `.claude/settings.local.json`, 36 `managed-settings.json`, 8 `\".claude.json\"` - the claim's four numbers reproduce exactly. csift's predicate at src/files/external.rs:25-32 accepts only basenames `settings.json` / `settings.local.json` under a `.claude` parent, so it rejects `managed-settings.json` and `.claude.json` by construction. Silent-write clause observed live: `csift files . --by timeline` prints 3 `external write` rows, e.g. `L4973 2026-06-29 11:51:38 AEST(UTC+10) turn 20 external write ~/.claude/settings.json (inferred: file-history v3->v4 with no tool record in L3896..L4973)`. File-history census over the whole corpus: 4065 file-history records, 1668 distinct tracked paths, 11 of them settings-family by csift's predicate; the six other tracked paths whose basename merely contains the word `settings` are ordinary source and note files that csift correctly rejects, and neither `managed-settings.json` nor `.claude.json` is file-history-tracked anywhere in this corpus. Both csift code sites (src/files/external.rs:10-14 and 25-32) match the claimed snippets verbatim.",
"rule": "One count per string-table occurrence for the binary census (grep -o over the strings dump, then uniq -c). One count per distinct tracked path for the file-history census: a path key inside a file-history-snapshot / file-history-delta record whose value object carries `version`, `content` or `hash`. One count per printed timeline row for the external-write observation.",
"note": "All four census numbers reproduce exactly, the csift predicate's two-basename scope is confirmed verbatim, and the \"no tool record\" clause is now instrument-backed rather than asserted (3 external-write rows on ~/.claude/settings.json in the csift repo's own scope, each naming a file-history version transition with no tool record in the interval). One boundary on the claim's `depends`: the miss it predicts for `managed-settings.json` and `.claude.json` is structurally real (csift's predicate rejects both) but unobserved here - neither path is file-history-tracked in this corpus, so nothing could have been mis-reported. Deciding whether the miss ever bites needs a machine that actually runs managed settings (MDM/policy-installed) or whose `.claude.json` is tracked."
}
]
},
{
"id": "FH-040",
"area": "freshness-file-history",
"behavior": "Token-cap truncation is reachable only on a whole-file read - the guard is `Ge=(f??1)<=1&&_===void 0&&v===void 0`, i.e. offset<=1 AND limit undefined AND PAGES undefined (`v` is the `pages` parameter, not a second limit) - and on that path `startLine` is forced to `Math.max(1,f)`. Because the tool entry defaults `offset:n=1`, `f` is always a number and the guard restricts it to <=1, so a truncated echo always reports the literal integer `startLine:1` (never null or NaN).",
"depends": "csift's `is_full` requires `start_line == 1`, which a truncated echo always satisfies, so `startLine` can never be the guard that demotes a capped read; only `observed >= total` stands between a capped page and a full-snapshot anchor.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "181",
"snippet": " let is_full = start_line == 1 && observed >= total && total > 0;"
}
],
"instrument": "Corpus: for every echo carrying `truncatedByTokenCap`, count `startLine != 1`; one observation per echo. Measured now over all of ~/.claude/projects: 0 of 174. Binary: the `let ht={type:\"text\",file:{filePath:n,content:De,numLines:Ne,startLine:Ue!==void 0?Math.max(1,f):f` construction, reachable via `python3 -c` seeking that literal in a `strings -a` dump.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 seeking `Ge=(f??1)<=1&&_===void 0&&v===void 0` in the raw 2.1.258 executable and printing 2600 bytes of preceding context, plus a regex sweep of the preceding 30000 bytes for the parameter destructuring; grep -oE 'startLine:Ue!==void 0\\?Math\\.max\\(1,f\\):f.{0,160}' over the strings dump; then python3 over ~/.claude/projects counting echoes with a `toolUseResult.file` object containing `truncatedByTokenCap` and, among them, those whose `startLine` is not 1.",
"observed": "Guard verbatim: `De=me,Ne=_e,Fe=_,Ue,je=!1,Ge=(f??1)<=1&&_===void 0&&v===void 0;try{await Ejn(me,d,I,F.credentials,Ge)}catch(Ot){if(Ot instanceof jue&&Ge){` - truncation is reachable only when Ge holds. Destructuring: `async function vGo(e){let{file_path:n,fullFilePath:r,resolvedFilePath:o,ext:d,offset:f,limit:_,pages:v,maxSizeBytes:C,maxTokens:I,context:F,messageId:B,io:U}=e` - so f=offset, _=limit, v=PAGES, not a second limit. Tool entry: `async function TGo({file_path:e,offset:n=1,limit:r=void 0,pages:o},d,f)` - offset defaults to 1. Echo construction: `let ht={type:\"text\",file:{filePath:n,content:De,numLines:Ne,startLine:Ue!==void 0?Math.max(1,f):f,totalLines:ke,...Ue!==void 0&&{truncatedByTokenCap:!0}}`. Corpus: 174 truncated echoes, 0 with startLine != 1. csift code site src/recover/carriers.rs:181 matches the claimed snippet verbatim.",
"rule": "One observation per echo: a record whose `toolUseResult.file` is an object containing the key `truncatedByTokenCap`; the echo counts against the claim iff its `startLine` is anything other than the integer 1 (a null or NaN-serialised value would therefore have been counted).",
"note": "The quoted guard is byte-exact and the conclusion is confirmed twice over, on the binary and on 174 corpus echoes. Two corrections: the parenthetical gloss \"offset<=1, no limit\" drops the third conjunct, which is the `pages` parameter (PDF page selection), and the guarantee is stronger than the claim states - the tool-entry default `offset:n=1` rules out the `Math.max(1,undefined)` NaN edge, so startLine is always exactly 1. The claim's `depends` on csift stands unchanged: with startLine pinned to 1, only `observed >= total` separates a capped page from a full-snapshot anchor - and FH-043's experiment shows that comparison also passes."
}
]
},
{
"id": "FH-041",
"area": "freshness-file-history",
"behavior": "The default file-read output cap is 25000 tokens (`var gYr=25000`), overridable per-process by `CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS`, and the default max file size is 262144 bytes (`var nhe=262144`); both defaults are also remotely overridable by a gate payload carrying `maxTokens`/`maxSizeBytes`.",
"depends": "The cap is a moving target, so csift must never hard-code a token threshold to infer truncation - the only stable instrument is the `truncatedByTokenCap` key or the banner, both of which csift currently ignores in `recover`.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "179-181",
"snippet": " let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;"
}
],
"instrument": "`strings -a <the Claude Code 2.1.258 executable> | grep -o 'var gYr=[0-9]*'` prints `var gYr=25000`; the same grep for `var nhe=[0-9]*` prints `var nhe=262144`. The env override appears as `let e=a.CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS;if(e!==void 0&&e>0)return e` in the `hYr` function.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a <the Claude Code 2.1.258 executable> > dump; grep -o 'var gYr=[0-9]*' dump; grep -o 'var nhe=[0-9]*' dump; grep -oE 'function hYr\\(\\)\\{.{0,300}' dump; grep -oE '.{160}maxSizeBytes.{0,160}' dump.",
"observed": "`var gYr=25000` and `var nhe=262144`, one occurrence each. Env override verbatim: `function hYr(){let e=a.CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS;if(e!==void 0&&e>0)return e;return}`. Gate payload verbatim: `function hX(){let e=Os();if(e.defaultFileReadingLimits!==void 0)return e.defaultFileReadingLimits;let n=P(\"tengu_amber_wren\",{}),r=typeof n?.maxSizeBytes===\"number\"&&Number.isFinite(n.maxSizeBytes)&&n.maxSizeBytes>0?n.maxSizeBytes:nhe,d=hYr()??(typeof n?.maxTokens===\"number\"&&Number.isFinite(n.maxTokens)&&n.maxT...` and the memoised result `e.defaultFileReadingLimits={maxSizeBytes:r,maxTokens:d,includeMaxSizeInPrompt:f,targetedRangeNudge:_}`. csift code site src/recover/carriers.rs:179-181 matches the claimed snippet verbatim.",
"rule": "Exact `var <name>=<digits>` byte match over the strings dump, one occurrence each; override paths read verbatim from the bytes of the two functions that compute the effective limits.",
"note": "Both constants and both override paths confirmed byte-exact. Three precision points a rerunner should carry: the env var overrides only the TOKEN cap (there is no env override for maxSizeBytes); the env wins over the gate (`d=hYr()??(gate maxTokens)`) while maxSizeBytes takes the gate value or falls back to `nhe`; and a per-call override also exists (`fileReadingLimits:{maxTokens:1/0,maxSizeBytes:268435456}` is passed on an inner call), which the claim does not mention but which reinforces its point that the cap is a moving target and no threshold should be hard-coded."
}
]
},
{
"id": "FH-042",
"area": "freshness-file-history",
"behavior": "Truncation has TWO branches: a line branch that keeps whole lines (`nt=gn.slice(0,yn).join('\\n')`) and a character branch for files whose first line alone exceeds the cap (`nt=me.slice(0,pn)`) that cuts MID-LINE, drops a trailing lone high surrogate, and then reports `numLines` as `newline-count + 1`, counting the truncated fragment as a whole line. BOTH branches carry up to six 0.7x shrink retries (`yn*0.7` on the line branch, `pn*0.7` on the character branch); the character branch is entered when the line branch's result still exceeds the cap or is blank (`if(Tn(nt)>I||nt.trim()===\"\")`).",
"depends": "csift's `split_lines` treats every line of `content` as ground truth and writes it into the sparse buffer, so on the character branch the LAST spliced line is a partial line presented as a known line - a fabrication the anti-fabrication guards do not catch.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "177-178",
"snippet": ") {\n let lines: Vec<String> = split_lines(content);"
}
],
"instrument": "Seek the literal `De=nt,Ne=je?` in a raw byte read of the 2.1.258 executable and print the surrounding ~1400 bytes; the two branches, the six-retry loops and the `je` flag are one expression. Corpus evidence that the character branch fires: count records whose `attachment.type` is `read_truncation_notice` and whose text carries the banner `this file has very long lines and cannot be paginated by line` - measured 2026-09-02: 31, all at CC 2.1.211, plus 3 older `tool_result` string carriers (2.1.159, 2.1.177, 2.1.231).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 seeking the literal `De=nt,Ne=je?` in the raw 2.1.258 executable and printing 1400 bytes before and 1500 after; then python3 walking ~/.claude/projects, byte-prefiltering each line on the exact banner literal `this file has very long lines and cannot be paginated by line`, parsing the survivors and bucketing them by record `type`, by `version`, and by which carrier field holds the banner.",
"observed": "Both branches in one expression: line branch `let gn=me.split('\\n'),cn=Math.max(0.5,me.length/Math.max(1,Ot.tokenCount)),Tn=(pn)=>pn.length/cn,yn=Math.max(1,Math.min(gn.length,Math.floor(gn.length*I/Math.max(1,Ot.tokenCount)*0.85))),nt=gn.slice(0,yn).join('\\n');for(let pn=0;pn<6;pn++){if(Tn(nt)<=I||yn<=1)break;yn=Math.max(1,Math.floor(yn*0.7)),nt=gn.slice(0,yn).join('\\n')}`; char branch, entered on `if(Tn(nt)>I||nt.trim()===\"\")`: `let pn=Math.max(1,Math.floor(I*cn*0.85));for(let fn=0;fn<6;fn++){if(nt=me.slice(0,pn),Tn(nt)<=I)break;pn=Math.max(1,Math.floor(pn*0.7))}let hn=nt.charCodeAt(nt.length-1);if(hn>=55296&&hn<=56319)nt=nt.slice(0,-1);je=!0`; then `De=nt,Ne=je?_n(nt,'\\n')+1:yn`. Corpus with the exact banner literal: 52 records total - 31 records whose `attachment.type` is `read_truncation_notice` (all at CC 2.1.211), 3 `tool_result` string carriers (one each at 2.1.159, 2.1.177, 2.1.231), and 18 records at CC 2.1.258 that are this verification session's own Bash tool output quoting the binary. csift code site src/recover/carriers.rs:177-178 matches the claimed snippet verbatim.",
"rule": "Corrected counting rule, contamination-proof: count one observation per record whose `attachment.type` is exactly `read_truncation_notice` AND whose serialized record contains the banner literal `this file has very long lines and cannot be paginated by line`. A bare needle count over all records is NOT rerunnable - it also catches any session that merely quoted the banner, including the verifying session itself.",
"note": "The mechanism is byte-exact as claimed, including the mid-line cut, the lone-high-surrogate drop and the `numLines = newline-count + 1` miscount. Three corrections. (1) The six 0.7x shrink retries are on BOTH branches, not only the line branch. (2) The stated count of 31 is right but its stated counting rule is not rerunnable: a bare needle count now returns 52 because 18 of those records are the verifying session's own grep output quoting the binary - self-contamination that will recur for anyone who greps the binary in a Claude Code session. Restricting to `read_truncation_notice` attachments reproduces 31 exactly and is contamination-proof. (3) A detail the claim does not state, measured in the FH-043 experiment: the banner is NOT inside `toolUseResult.file.content` - it rides the notice attachment and the `<system-reminder>` prefix on the rendered tool_result, which is why csift's carrier reader never sees it."
}
]
},
{
"id": "FH-043",
"area": "freshness-file-history",
"behavior": "Because the character branch reports `numLines` as the line count of the partial page while `totalLines` stays the whole file's count, a one-very-long-line file over the token cap yields `startLine:1, numLines:1, totalLines:1` with `truncatedByTokenCap:true` - numerically indistinguishable from a complete read. Directly reproduced on Claude Code 2.1.258 with a 66000-character single-line file: the echo carried exactly that quartet and only 32742 of 66000 characters.",
"depends": "csift's `is_full = start_line == 1 && observed >= total && total > 0` promotes that capped page to a `FullSnapshot` anchor. Confirmed end-to-end on csift 0.10.0: `csift recover --file` exits 0, reports `1 lines, complete`, and writes 32742 of the source's 66000 characters - truncated bytes handed back as a complete restorable file, with no disclosure. The fix is to demote whenever `truncatedByTokenCap` is present.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "178-182",
"snippet": " let lines: Vec<String> = split_lines(content);\n let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;\n if is_full {"
},
{
"path": "src/recover/types.rs",
"lines": "17-22",
"snippet": " /// Full ground-truth content (an anchor): a Write result, a full Read\n /// (`startLine==1 && numLines==totalLines`), or a `file` attachment.\n FullSnapshot {\n content: String,\n total_lines: usize,\n source: SnapSource,"
}
],
"instrument": "Write a single-line file with no newlines, sized over the read token cap and under the byte cap (66000 hex characters is comfortably both); have Claude Code Read it once in an isolated run; read the resulting echo's `toolUseResult.file` fields from that run's transcript; then run `csift recover <transcript> --file <file> --out <out>` and byte-compare. Measured 2026-09-02 on CC 2.1.258 + csift 0.10.0: echo `startLine:1 numLines:1 totalLines:1 truncatedByTokenCap:true` with 32742 of 66000 characters; recover reported `complete` and returned a strict prefix.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Built a 66000-character single-line file (0 newlines, 66000 bytes, under the 262144-byte size cap and over the 25000-token read cap) in a temp dir; ran `claude -p \"Use the Read tool exactly once on <that file> and then reply with only the word DONE. Do not print any file content.\" --allowedTools Read` on Claude Code 2.1.258; located that run's top-level transcript under ~/.claude/projects by mtime and read the echo's `toolUseResult.file` fields with python3; then ran `csift recover <that transcript> --file <that file> --out <out>` on csift 0.10.0 and byte-compared the recovered output against the source.",
"observed": "Echo on disk, CC version 2.1.258: `startLine: 1`, `numLines: 1`, `totalLines: 1`, `truncatedByTokenCap: True`, `content` = 32742 characters containing 0 newlines, and that content is a strict prefix of the 66000-character source. csift end-to-end: `csift recover ... --file ... --out ...` exited 0 and reported on stderr `(recovered <file> -> <out>, 1 lines, complete; no bash mutation of this file and no opaque mutating-class command detected in the window)`; the recovered file is 32742 characters against the source's 66000 and is a STRICT PREFIX of it. Corpus baseline before the experiment: 174 truncated echoes, 0 with `startLine==1 && numLines>=totalLines`. csift code sites src/recover/carriers.rs:178-182 and src/recover/types.rs:17-22 match the claimed snippets verbatim.",
"rule": "One observation per echo: a record whose `toolUseResult.file` object contains `truncatedByTokenCap` AND has `startLine == 1` AND `numLines >= totalLines`. The end-to-end check is a byte comparison: recovery is a false 'complete' iff `csift recover` exits 0, its report says `complete`, and the recovered bytes are a strict prefix of (not equal to) the source bytes.",
"note": "The claim's mechanism is exactly right and its hedge is now obsolete. The corpus baseline still reproduces (0 of 174), but a purpose-built read exercised the hazard on the current binary and the current csift, and the end-to-end consequence the claim only predicted - `recover` reporting `complete` while returning a strict prefix - actually happened. This moves the finding from a structural argument to a reproduced defect; the counting rule above is the rerun recipe."
}
]
},
{
"id": "FH-044",
"area": "freshness-file-history",
"behavior": "Claude Code's own transcript-replay treats a read as partial if ANY of three carriers says so: `toolUseResult.file.truncatedByTokenCap===true`, a `read_truncation_notice` attachment naming that `tool_use_id`, or the rendered `tool_result` text starting with `<system-reminder>` immediately followed by the `[Truncated: PARTIAL view — ` banner prefix (the separator is U+2014, not a hyphen).",
"depends": "csift reads none of the three in `recover`, so a resumed session and csift disagree about which reads were complete; the three-carrier OR is the ready-made specification for a csift truncation gate.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "176-183",
"snippet": " events: &mut Vec<FileEvent>,\n) {\n let lines: Vec<String> = split_lines(content);\n let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;\n if is_full {\n events.push(FileEvent {"
}
],
"instrument": "Seek the literal `let W=C.toolUseResult?.file?.truncatedByTokenCap===!0||v.has(I.tool_use_id)||I.content.startsWith(` in a `strings -a` dump of the 2.1.258 binary; the surrounding function rebuilds `readFileState` from persisted messages and sets `isPartialView` from that disjunction.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 scanning the raw 2.1.258 executable for every occurrence of `truncatedByTokenCap===!0`, printing 320 bytes before and 420 after each, plus 2100 bytes before the first occurrence to recover the construction of the notice set; then python3 seeking `lfe=\"[Truncated` and `[Truncated: PARTIAL view` in the raw bytes.",
"observed": "4 occurrences of `truncatedByTokenCap===!0` in the binary. The replay one is verbatim: `let W=C.toolUseResult?.file?.truncatedByTokenCap===!0||v.has(I.tool_use_id)||I.content.startsWith(\"<system-reminder>\"+lfe),fe=I.content.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/g,\"\")...` and the setter `o.set(F.filePath,{content:fe,timestamp:me,offset:_e?1:F.offset??1,limit:_e?void 0:F.limit,...W&&{isPartialView:!0}})`. The notice set is built in the same function: `function zgt(e,n,r=XEo){let o=qw(r),d=new Map,f=new Map,_=new Map,v=new Set;for(let C of e){if(C.type===\"attachment\"&&C.attachment.type===\"read_truncation_notice\"){v.add(C.attachment.toolUseID);continue}...` - the function logs under the name `extractReadFilesFromMessages`, i.e. it rebuilds readFileState from persisted messages. Banner prefix: `var lfe=\"[Truncated: PARTIAL view \\u2014 \"` - the separator is U+2014, confirmed. csift code site src/recover/carriers.rs:176-183 matches the claimed snippet verbatim.",
"rule": "One occurrence per byte match of `truncatedByTokenCap===!0` over the whole executable; the replay disjunction is the occurrence whose enclosing function also constructs a Set from `read_truncation_notice` attachments and writes `isPartialView`.",
"note": "All three carriers of the OR confirmed byte-exact in one expression, together with the construction of the middle one (a Set of `attachment.toolUseID` from `read_truncation_notice` attachments) and the U+2014 separator in the banner prefix constant. The claim's characterisation of the function is also right: it rebuilds `readFileState` from persisted messages and stamps `isPartialView` from the disjunction. Worth recording for a rerunner: the other three occurrences of the same literal are unrelated consumers - one refuses to build an `edited_text_file` freshness attachment from a truncated re-read, and one falls back on an at-mention path - so match on the enclosing function, not the literal alone."
}
]
},
{
"id": "FH-045",
"area": "freshness-file-history",
"behavior": "The `truncatedByTokenCap` key predates the `read_truncation_notice` attachment by roughly 56 patch releases: the key is observed from Claude Code 2.1.150 while the earliest notice attachment in the same corpus is 2.1.206.",
"depends": "A csift truncation gate should key on the programmatic field first and treat the attachment and the `<system-reminder>` banner as corroboration, because only the field covers older transcripts.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "176-181",
"snippet": " events: &mut Vec<FileEvent>,\n) {\n let lines: Vec<String> = split_lines(content);\n let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;"
}
],
"instrument": "Corpus: bucket by the record's `version` field, counting one observation per echo carrying `truncatedByTokenCap` and one per record whose `attachment.type` is `read_truncation_notice`. Measured now: the key appears at 2.1.150 (2), 2.1.156 (2), 2.1.159 (10) and onward; the attachment first at 2.1.206 (9).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": "2.1.150",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 walking ~/.claude/projects, byte-prefiltering each jsonl line on b'truncatedByTokenCap' and b'read_truncation_notice', parsing the survivors, and bucketing by the record's top-level `version` field: one bucket entry per echo whose `toolUseResult.file` object contains `truncatedByTokenCap`, one per record whose `attachment.type` is `read_truncation_notice`.",
"observed": "174 truncated echoes across 25 Claude Code versions, earliest buckets 2.1.150 (2), 2.1.156 (2), 2.1.159 (10), 2.1.170 (3), 2.1.177 (19) - reproducing the claim's first three exactly. 183 notice-attachment records across 17 versions, earliest bucket 2.1.206 (9) - reproducing the claim's first bucket exactly. 2.1.206 minus 2.1.150 is 56 patch releases. csift code site src/recover/carriers.rs:176-181 matches the claimed snippet verbatim.",
"rule": "One observation per echo (a record whose `toolUseResult.file` object contains the key `truncatedByTokenCap`) and one per notice record (a record whose `attachment.type` equals `read_truncation_notice`), each bucketed by the record's own top-level `version` string; the earliest non-empty bucket is the first-seen version.",
"note": "Both first-seen versions and all four quoted bucket counts reproduce exactly, and the 56-release gap is arithmetic on them. One honest boundary the claim does not state: these are FLOORS bounded by the Claude Code versions this machine actually ran, not true first-release versions - the corpus jumps 2.1.150 -> 2.1.156 -> 2.1.159 with nothing between, so either field could have shipped in an unrun earlier version. Deciding the true introduction release would need release-artifact archaeology across every 2.1.x build, which no instrument on this machine can do. The claim's operational conclusion is unaffected: on any corpus, the programmatic field covers strictly older transcripts than the attachment."
}
]
},
{
"id": "FH-046",
"area": "freshness-file-history",
"behavior": "The external-edit scanner skips a file entirely when its remembered read state carries an `offset` or a `limit`, and skips it again when a fresh read comes back with `truncatedByTokenCap===true`; a token-cap-truncated read also writes `limit` and `isPartialView:true` into that state.",
"depends": "So an `edited_text_file` boundary can only ever exist for a file that was read WHOLE and uncapped - csift's hard external-edit boundary is structurally absent for every windowed or token-capped file, which `recover`'s opaque accounting must not read as evidence of no change.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "216-222",
"snippet": " let atype = att.get(\"type\").and_then(serde_json::Value::as_str);\n\n // (7a) edited_text_file → an external edit (hard boundary).\n if atype == Some(\"edited_text_file\") {\n let path = att\n .get(\"filename\")\n .or_else(|| att.get(\"filePath\"))"
}
],
"instrument": "Seek `if(v.offset!==void 0||v.limit!==void 0)return null` and `if(U.data.file.truncatedByTokenCap===!0)return null` in a `strings -a` dump of the 2.1.258 binary - both guards live in the same scanner function, a few hundred bytes apart. The state write is the `j.set(r,{content:De,timestamp:Math.floor(Pe),offset:f,limit:Fe,...Ue!==void 0&&{isPartialView:!0}})` expression on the Read path.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a -n 6 ~/.local/share/claude/versions/2.1.258 > cc258.strings.txt && rg -o -a '.{700}set!==void 0\\|\\|v\\.limit!==void 0\\)return null.{700}' cc258.strings.txt && rg -o -a '.{420}isPartialView:!0.{100}' cc258.strings.txt && python3 -c \"b=open('<the 2.1.258 executable>','rb').read(); i=b.find(b'j.set(r,{content:De,timestamp:Math.floor(Pe),offset:f,limit:Fe'); print(repr(b[i-2600:i+200]))\" && sed -n '205,235p' src/recover/carriers.rs",
"observed": "Both guards are verbatim in one function whose only return value is the external-edit attachment: 'async function QSr(e){...let v=e.readFileState.get(_);if(!v)return null;if(v.offset!==void 0||v.limit!==void 0)return null;...let U=await im.call(F,e);if(U.data.type===\"text\"){if(U.data.file.truncatedByTokenCap===!0)return null;if(z$(v,U.data.file.content))return null;let j=EJt(v.content,U.data.file.content);if(j===\"\")return null;return{type:\"edited_text_file\",filename:C,snippet:j}}'. The Read state write is verbatim: 'j.set(r,{content:De,timestamp:Math.floor(Pe),offset:f,limit:Fe,...Ue!==void 0&&{isPartialView:!0}});', and the 2600 preceding bytes show the token-cap branch assigning 'Fe=Ne,Ue=' the truncation notice (either 'showing lines 1-${Ne} of ${ke} total (${Ot.tokenCount} tokens, cap ${I})' or 'showing the first ${nt.length} of ${me.length} characters'), so a capped read writes both limit and isPartialView:!0. The Read entry function binds them as parameters: 'async function vGo(e){let{file_path:n,fullFilePath:r,resolvedFilePath:o,ext:d,offset:f,limit:_,pages:v,...}=e' - f IS the caller's offset argument. csift site: src/recover/carriers.rs:216-222 matches the ledger snippet verbatim (216 'let atype = att.get(\"type\").and_then(serde_json::Value::as_str);', 219 'if atype == Some(\"edited_text_file\") {').",
"rule": "One matched byte string per guard in the shipped executable, both inside the single function that returns {type:\"edited_text_file\"}; a matched string is the observation. Code site checked by exact line number plus byte-identical snippet.",
"note": "Both guards and the state write survive intact at 2.1.258. Supporting corpus fact: 1339 edited_text_file attachment records exist across the transcripts scanned, so the scanner does fire in practice - consistent with a whole-file read leaving offset/limit undefined in the remembered state."
}
]
},
{
"id": "FH-047",
"area": "freshness-file-history",
"behavior": "The unchanged-file dedup has TWO branches, both gated by one lookup that returns undefined - and so disables both - under the tengu_read_dedup_killswitch gate, on a remote call, or when dedupUnchangedReads===false. Branch A (source:\"seeded\") fires when the remembered entry is startup-seeded, is NOT a partial view, and the NEW call asks for the whole file (offset===1 and limit===undefined); it compares the file's timestamp for exact equality with the remembered one. Branch B fires when the remembered entry is NOT a partial view AND itself carries a defined offset (i.e. it came from a windowed Read, not from an edit/write-seeded entry), the new call's offset and limit match the remembered ones exactly, and the file's timestamp is exactly equal to the remembered one. Both branches additionally require a pending-handover guard to be false. Because a whole-file Read stores offset===undefined, branch B never dedups a plain whole-file re-read.",
"depends": "The `!isPartialView` precondition is why a token-cap-truncated read is always re-read in full later, which is the only reason csift's contentless `file_unchanged` handling has not yet produced a wrong recovery.",
"code": [
{
"path": "src/recover/buffer.rs",
"lines": "85-86",
"snippet": " let norm_total = self.normalize_total(total_lines);\n self.seen_total_lines = Some(norm_total.max(self.seen_total_lines.unwrap_or(0)));"
}
],
"instrument": "Seek `type:\"file_unchanged\"` in a `strings -a` dump of the 2.1.258 binary, skipping the zod `describe(` occurrence, and print the preceding 1100 bytes: the two dedup branches, the `!fe.isPartialView` guard, the `fe.offset===n&&fe.limit===r` equality and the `tengu_read_dedup_killswitch` gate are all in that span.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"b=open('<the 2.1.258 executable>','rb').read(); [print(repr(b[b.find(s):b.find(s)+330])) for s in (b'let fe=P(\\\"tengu_read_dedup_killswitch\\\"', b'if(fe&&fe.seededFromContext', b'if(fe&&!fe.isPartialView&&fe.offset!==void 0)')]\" && rg -n 'let norm_total = self.normalize_total' src/recover/buffer.rs",
"observed": "Three verbatim spans. Gate: 'let fe=P(\"tengu_read_dedup_killswitch\",!1)||d.remoteCall!==void 0||d.dedupUnchangedReads===!1?void 0:_.get(j);'. Branch A: 'if(fe&&fe.seededFromContext&&!fe.isPartialView&&n===1&&r===void 0)try{let Ie=await RSr(j,d.storageV5);if(Ie===fe.timestamp&&!ke(Ie)){...return ...{data:{type:\"file_unchanged\",file:{filePath:j},source:\"seeded\"}}}}catch{}'. Branch B: 'if(fe&&!fe.isPartialView&&fe.offset!==void 0){if(fe.offset===n&&fe.limit===r)try{let Pe=await Bx(j);if(Pe===fe.timestamp&&!ke(Pe)){...return{data:{type:\"file_unchanged\",file:{filePath:e}}}}}catch{}}'. The enclosing function binds n=offset and r=limit ('let we={file_path:e,fullFilePath:j,ext:U,offset:n,limit:r,pages:o,...}' immediately after branch B). The zod result schema confirms the payload carries no content: 'c({type:x(\"file_unchanged\"),file:c({filePath:i().describe(\"The path to the file\")}),source:x(\"seeded\").optional()...})'. csift site: src/recover/buffer.rs:85-86 matches the ledger snippet verbatim.",
"rule": "Every occurrence of the literal 'file_unchanged' in the executable was enumerated (7 total: 1 zod schema, 1 tool_result mapper, 2 dedup returns, 3 consumers); the two returns inside the Read entry function are the dedup, and each branch's full guard chain was read from the raw bytes.",
"note": "The claim's single-branch summary is right about branch B but omits the seeded branch, the extra 'prior entry has a defined offset' precondition, the pending-handover guard, and that the mtime test is exact equality rather than 'unchanged'. The csift-side consequence (the contentless payload) is unaffected."
}
]
},
{
"id": "FH-048",
"area": "freshness-file-history",
"behavior": "A contentless `file` echo drives csift's push_read_event to observed = 0 and total = 1, producing a PartialRead that reports the file as one line long. THREE persisted Read result variants carry a file.filePath with no content string and so take this path: file_unchanged (120 records), parts - the PDF page-extraction result (26 records), and pdf (4 records).",
"depends": "`SparseBuffer::splice` raises `seen_total_lines` with a `.max()` against the existing value, so the bogus total of 1 cannot shrink a real total learned from an earlier read - that `.max()` is the only thing standing between a `file_unchanged` echo and a wrong `recover --coverage` denominator.",
"code": [
{
"path": "src/recover/buffer.rs",
"lines": "10-86",
"snippet": " }\n let norm_total = self.normalize_total(total_lines);\n self.seen_total_lines = Some(norm_total.max(self.seen_total_lines.unwrap_or(0)));"
},
{
"path": "src/recover/carriers.rs",
"lines": "178-181",
"snippet": " let lines: Vec<String> = split_lines(content);\n let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;"
}
],
"instrument": "Read the csift unit test `src/recover/tests/coverage.rs` for the coverage arithmetic, then re-derive by hand: with `content` empty, `split_lines` returns an empty vector (pinned by `split_lines_drops_only_trailing_newline` in `src/recover/tests/render.rs`), so `observed=0` and `total = 0.max(1+0) = 1`, and `is_full` is false because `0 >= 1` is false.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/recover/buffer.rs, the seen_total_lines max() guard"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift --claude-home ./fx2 recover --file /tmp/demo/x.txt --coverage # fixture fx2 = one synthetic project dir holding one transcript: a genuine user line, a Read tool_use, and a user tool_result whose toolUseResult is {\"file\":{\"filePath\":\"/tmp/demo/x.txt\"}} (the contentless echo) ;; csift --claude-home ./fx recover --file /tmp/demo/x.txt --coverage # fixture fx = the same, but with a prior Read tool_result whose toolUseResult is {\"file\":{\"filePath\":\"/tmp/demo/x.txt\",\"content\":\"alpha\\nbeta\\ngamma\\n\",\"startLine\":1,\"numLines\":3,\"totalLines\":3}} ;; a python3 walk of ~/.claude/projects tallying records whose toolUseResult.file has a filePath, keyed by (toolUseResult.type, whether file.content is a string)",
"observed": "Contentless echo alone: 'recoverable: 0/1 lines (0%) fragments: 1' / 'covered line ranges: (none)' / 'events: 1 read (0 full, 1 windowed)' - a PartialRead with a total of 1 and nothing observed. Full 3-line read then the same echo: 'recoverable: 3/3 lines (100%) fragments: 1' / 'covered line ranges: [1..3]' / 'events: 2 read (1 full, 1 windowed)' - the max() guard held the real total. Corpus: records carrying toolUseResult.file.filePath with NO string content = 120 of type file_unchanged, 26 of type parts, 4 of type pdf; 8608 of type text DO carry a content string. Schemas confirm the shapes: parts persists file:{filePath, originalSize, count, outputDir}, pdf persists file:{filePath, base64, originalSize}. csift sites: src/recover/buffer.rs:84-86 and src/recover/carriers.rs:178-181 match the ledger snippets verbatim.",
"rule": "One count per transcript RECORD whose toolUseResult is an object with a file object containing filePath, bucketed by toolUseResult.type and by whether file.content is a string. Fixture verdicts are read off csift's own --coverage line: 'recoverable: <known>/<total> lines'.",
"note": "Verified end to end by running csift, not by reading it: the echo-only fixture reports 0/1 lines, and the same echo after a real 3-line read still reports 3/3, which is the seen_total_lines max() guard doing exactly what the claim says. Only the variant enumeration needed widening - `pdf` was missing."
}
]
},
{
"id": "FH-049",
"area": "freshness-file-history",
"behavior": "The gutter separator is a TAB by default and becomes a COLON only when the tab-aware mode is on AND the whole CONTENT blob being rendered starts with a tab or contains a newline immediately followed by a tab (`let i=n&&(e.startsWith(\"\\t\")||e.includes(\"\\n\\t\"))?\":\":\"\\t\"`) - i.e. the separator is chosen ONCE per blob, when any line of it is tab-indented, not per line. The tab-aware mode is itself behind a gate that defaults to false (`function iz(){return kU(\"tengu_tab_read_sep\",!1)}`). Claude Code's own stripper accordingly accepts three separators: `function adr(e){return e.match(/^\\s*\\d+[\\u2192\\t:](.*)$/)?.[1]??e}`.",
"depends": "csift's `strip_gutter` accepts only TAB and U+2192 and `continue`s on anything else, so a colon-guttered `edited_text_file` snippet line is silently dropped from the external-edit snippet - no fabrication, but a lost line with no disclosure.",
"code": [
{
"path": "src/recover/diff.rs",
"lines": "199-206",
"snippet": " let rest = &trimmed[digits.len()..];\n let text = if let Some(t) = rest.strip_prefix('\\t') {\n t\n } else if let Some(t) = rest.strip_prefix('\\u{2192}') {\n t\n } else {\n continue;\n };"
},
{
"path": "src/recover/diff.rs",
"lines": "185-189",
"snippet": "/// Strip a leading line-number gutter from each line of a cat -n style snippet. Handles\n/// BOTH the TAB gutter (`\\d+\\t<text>`, what current CC Read content uses) and the arrow\n/// gutter (`\\d+→<text>`, an older form). Returns `(file_line_no, text)` pairs; a line\n/// with no recognizable gutter is skipped (we never fabricate a number).\npub(crate) fn strip_gutter(snippet: &str) -> Vec<(usize, String)> {"
}
],
"instrument": "Read the condition from the RAW executable bytes, not from a `strings` dump: the template literal holds a real newline followed by a real TAB, and `strings` splits the line at the newline and drops the one-byte tab remainder, which makes the condition read as the much broader `e.includes(\"\\n\")`. Use python3 to slice around `function j8t({content:e,startLine:r`.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (read-window fidelity probe)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"b=open('<the 2.1.258 executable>','rb').read(); i=b.find(b'function j8t({content:e,startLine:r'); print(repr(b[i:i+330]))\" ;; python3 -c \"... b.find(b'function EJt(e,n){let r=rne(\\\"file.txt\\\"') ...\" ;; rg -o -a 'function iz\\(\\).{0,60}' cc258.strings.txt ;; a python3 walk of ~/.claude/projects collecting, for every attachment record of type edited_text_file, the character following the leading digits of each snippet line",
"observed": "Raw bytes: 'function j8t({content:e,startLine:r,tabAwareSeparator:n=!1}){if(!e)return\"\";let i=n&&(e.startsWith(\"\\t\")||e.includes(`\\n\\t`))?\":\":\"\\t\",s=[],o=r,c=0,f=e.indexOf(`\\n`);while(f!==-1)s.push(W8t(e.slice(c,f),o++,i)),c=f+1,f=e.indexOf(`\\n`,c);return s.push(W8t(e.slice(c),o,i)),s.join(`\\n`)}function W8t(e,r,n){let i=e.endsWith(\"\\r\")?e.slice(0,-1):e;return`${r}${n}${i}`}function adr(e){return e.match(/^\\s*\\d+[\\u2192\\t:](.*)$/)?.[1]??e}'. The mode is a gate: 'function iz(){return kU(\"tengu_tab_read_sep\",!1)}'. The external-edit snippet builder routes through the same renderer: 'function EJt(e,n){...let o=iz(),d=r.hunks.map((B)=>({startLine:B.oldStart,content:...,tabAwareSeparator:o})).map(j8t).join(`\\n...\\n`);...}'. Corpus: 1339 edited_text_file attachment records; 85 empty snippets; 1254 non-empty snippets whose gutter separators form the single set {TAB}; 0 snippets containing a colon or U+2192 gutter; 151912 gutter lines observed, all TAB.",
"rule": "Separator = the single character following the leading digits of a snippet line matched by ^\\s*\\d+(.). One observation per SNIPPET, recorded as the SET of separators seen across that snippet's gutter lines (per-line totals reported separately). Binary claims are matched byte strings in the shipped executable.",
"note": "The corpus half reproduces exactly (1254 TAB-only snippets, 85 empty, 0 colon, 0 arrow). The binary half needed correcting twice: the condition is newline+TAB (any tab-indented line), not any newline, and the choice is per content blob rather than per line. Both csift sites are verbatim at src/recover/diff.rs:185-189 and 199-206, and the exposure the claim describes is real but currently unreachable in this corpus, since the colon separator needs a gate that is off by default."
}
]
},
{
"id": "FH-050",
"area": "freshness-file-history",
"behavior": "Claude Code strips a Write result of type update through stripForStorage, which rewrites the stored object to {...e, content:\"\", originalFile:null} unless one of TWO early returns fires: the content is already empty with an empty-or-absent originalFile, or the structuredPatch is an empty array with a null originalFile (the second guard is newer than 2.1.229). The strip is not applied at persist time to every result: it runs as a sweep over the message array that skips the most recent 200 messages (the interactive call site uses that default; the headless flush path passes a window of 0, stripping every eligible message). An update-type Write carrier can therefore land in the transcript with an EMPTY content string and no original content at all.",
"depends": "csift's carrier extractor treats any toolUseResult with a filePath and a content string but no oldString/newString as a full-snapshot anchor and calls buf.reset_to_full(content, ...). A stripped update carrier therefore resets the sparse reconstruction buffer to a zero-line file: reset_to_full clears the known-line map outright, so the prior content does not survive as explicit `??? lines A..B unknown` gaps - the total collapses too, and coverage reports 0/0 lines while --salvage reports no content seen at all.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "149-151",
"snippet": " // A Write result: full-content anchor.\n if let Some(content) = tur.get(\"content\").and_then(serde_json::Value::as_str) {\n let total = line_count(content);"
},
{
"path": "src/recover/replay.rs",
"lines": "89",
"snippet": " buf.reset_to_full(content, *total_lines, e.line_no);"
}
],
"instrument": "Measured incidence: 19 of 291 update carriers have content == '', all at versions 2.1.208 (16 of 110) and 2.1.211 (3 of 4); 0 of the 102 update carriers at versions after 2.1.211 are empty, including the one at 2.1.258. 1 of 2598 create carriers is empty (at 2.1.217, a genuinely empty file).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02 (carrier-shape probe)"
},
"first_seen_claude_code": "2.1.208",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import re;b=open('<the 2.1.258 executable>','rb').read();print(re.search(rb'stripForStorage\\(e\\)\\{if\\(typeof e!==\\\"object\\\"\\|\\|e===null\\)return e;if\\(e\\.type!==\\\"update\\\"\\)return e;[^}]{0,240}',b).group(0))\" ;; python3 -c \"... find(b'if(!F?.stripForStorage)continue') and every occurrence of the sweep function name ...\" ;; a python3 walk of ~/.claude/projects counting records whose toolUseResult is an object of type create/update, bucketed by whether content == '' and keyed by the record's version ;; csift --claude-home ./fx3 recover --file /tmp/demo/x.txt --coverage and --salvage on a fixture whose Write carrier is {\"type\":\"update\",\"filePath\":\"/tmp/demo/x.txt\",\"content\":\"\",\"originalFile\":null,\"structuredPatch\":[]} after a full 3-line read",
"observed": "Verbatim at 2.1.258: 'stripForStorage(e){if(typeof e!==\"object\"||e===null)return e;if(e.type!==\"update\")return e;if(e.content===\"\"&&(e.originalFile??\"\")===\"\")return e;if(Array.isArray(e.structuredPatch)&&e.structuredPatch.length===0&&e.originalFile===null)return e;return{...e,content:\"\",originalFile:null}}' - TWO early returns, and the same function at 2.1.229 has only the first, so the structuredPatch guard is newer than 2.1.229. The caller is a windowed sweep: 'function $_t(e,n,r=200,o=!1){let d=e.length-r;if(d<=0)return e;...if(v>=d||C.type!==\"user\"||C.isVirtual||C.toolUseResult==null||!Array.isArray(C.message.content))continue;...let B=(()=>{try{return F.stripForStorage(C.toolUseResult,o)}catch(j){return h(j),C.toolUseResult}})();...}' with two call sites: '$_t(Ko,Go.options.tools)' at query end (default window 200) and '$_t(o,je,0,!0)' on the headless flush path (window 0 = strip everything). Corpus: 291 update carrier records (272 non-empty, 19 empty) and 2598 create carrier records (2597 non-empty, 1 empty); every empty update carrier is at version 2.1.208 (16 of 110) or 2.1.211 (3 of 4); across versions after 2.1.211 there are 102 update carriers and 0 empty ones, including 1 at 2.1.258. Fixture: the stripped update carrier after a full 3-line read yields 'recoverable: 0/0 lines (0%)' / 'covered line ranges: (none)' / 'events: 1 read (1 full, 0 windowed) - 1 write', and --salvage prints '(no content seen for this file in range)'. csift sites: src/recover/carriers.rs:149-151 and src/recover/replay.rs:89 match the ledger snippets verbatim.",
"rule": "One count per carrier RECORD: a transcript record whose toolUseResult is an object with type create or update; bucketed by content == '' and keyed by the record's own version field. Binary claims are matched byte strings in the shipped executable; the fixture verdict is read off csift's own --coverage line.",
"note": "The mechanism is present and byte-identical in 2.1.258, so this is not drift, but two things needed fixing. The strip is windowed - only messages older than the most recent 200 are swept at query end - which is why an append-only transcript sees it so rarely, and the corpus shows zero empty update carriers at any version after 2.1.211. And the csift consequence is harsher than the claim states: the prior lines are erased rather than turned into disclosed gaps, so the recovery reports a zero-length file rather than a holey one."
}
]
},
{
"id": "IMG-001",
"area": "images",
"behavior": "A pasted or attached image (and a tool-result screenshot) rides INLINE on the record as a `{type:\"image\", source:{type:\"base64\", media_type:\"image/png\", data:\"<base64>\"}}` block - the bytes live in the jsonl line itself and nothing is externalised to a sidecar file (verified against real `~/.claude/projects` data on 2026-06-16). One record commonly carries several such blocks.",
"depends": "csift's `image` decodes the inline bytes straight back to files and addresses each occurrence by the stable locator `L<line>i<n>` (the 1-based jsonl line of the carrying record plus the 1-based ordinal of the image within that record), which is stable because the transcript is append-only; an externalised-pointer assumption would make `image --out` produce nothing.",
"code": [
{
"path": "src/image.rs",
"lines": "3-6",
"snippet": "//! A user-pasted/attached image (and a tool-result screenshot) rides INLINE on a record as\n//! an `{type:\"image\", source:{type:\"base64\", media_type:\"image/png\", data:\"<base64>\"}}`\n//! block - verified against real `~/.claude/projects` data (2026-06-16). The bytes live in\n//! the jsonl, so `image` decodes them straight back to files; nothing is externalised."
},
{
"path": "src/image.rs",
"lines": "8-11",
"snippet": "//! Stable image id = `L<line>i<n>`: the 1-based JSONL line of the carrying record plus the\n//! 1-based ordinal of the image among that record's image blocks. It is stable because the\n//! transcript is append-only, and it is consistent with the `Lnnnnn` line references used\n//! across `recover` / `turns` / `search` (so an id surfaced there feeds straight back here)."
}
],
"instrument": "`csift image @<session>` lists every image with its media type and estimated bytes; cross-check against `rg -c '\"type\":\"base64\"' <transcript>`. Counting rule: one row per image block, direct blocks and tool_result-nested elements alike.",
"located": {
"claude_code": "2.1.258",
"csift": "0.3.0",
"source": "SPEC.md section 6.9; src/image.rs:3-6 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift show ~/.claude/projects/<enc>/<session>.jsonl --line 4351 --raw | jq -c '{type, role: .message.role, blocks: [.message.content[]? | {t: .type, src: (.source.type // null), mt: (.source.media_type // null), dlen: (.source.data|if .==null then null else length end)}]}' AND csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id L42693i1,L42103i1 --out out AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,80}type:\"base64\",media_type.{0,120}'",
"observed": "One `type:\"user\"` record carried blocks [text, image x6]; every image block was `source.type == \"base64\"` with `source.media_type` in {image/jpeg, image/png} and an inline `source.data` of 555588 / 359416 / 246952 / 279804 / 282420 / 307516 base64 characters. Extraction from a different transcript wrote a 450165-byte file that `file` reports as `JPEG image data ... 2000x1605` and a 217338-byte `PNG image data`, both byte-for-byte the sizes csift's est_bytes predicted. Claude Code 2.1.258 binary carries the constructor `{block:{type:\"image\",source:{type:\"base64\",media_type:ANe(d),data:d.toString(\"base64\")}},dimensions:m.dimensions}` and the media-type sniffer `function ANe(e){return pR(e)??\"image/png\"}`. Corpus-wide (14 project dirs, *.jsonl only) there are 3021 `\"media_type\"` occurrences and 0 image references with a non-base64 source kind.",
"rule": "One image block per `{type:\"image\"}` object on a record; source kind counted once per image reference from the csift JSON `source_kind` field; the binary strings are counted as present/absent, quoted verbatim.",
"note": "Both code sites exist verbatim at the claimed locations: src/image.rs:3-6 and src/image.rs:8-11. Nothing was externalised on any inspected record: the base64 payload sits in the transcript line itself, and `image --out` decoded it to valid image files without touching any sidecar. One record carrying several image blocks is confirmed directly (6 on one line; the per-line histogram over one transcript's 73 image-carrying lines runs 1,2,3,4,6,8,10,33,38 images per record)."
}
]
},
{
"id": "IMG-002",
"area": "images",
"behavior": "An image reaches the transcript through one of two carriers: a direct `{type:\"image\"}` content block (the user-sent / assistant case) or an `{type:\"image\"}` element nested inside a `tool_result` block's `content` ARRAY (a tool screenshot).",
"depends": "csift walks both carriers in document order under a single 1-based running ordinal, so the `L<line>i<n>` locator stays stable across the two shapes; handling only the direct block makes tool screenshots invisible to `image` and to `image --out`.",
"code": [
{
"path": "src/image/refs.rs",
"lines": "231-234",
"snippet": "/// Collect every image carried by one record, in document order: a direct `Block::Image`\n/// (the user-sent / assistant case) OR an `{type:\"image\"}` element nested in a\n/// `Block::ToolResult.content` array (a tool screenshot). `img_index` is a 1-based running\n/// ordinal across both sources, so the id `L<line>i<n>` is stable."
},
{
"path": "src/image/refs.rs",
"lines": "255-259",
"snippet": " Block::ToolResult {\n content: Some(content),\n ..\n } => {\n if let Some(arr) = content.as_array() {"
}
],
"instrument": "`csift image <target>` and compare its row count to a walk that counts `{\"type\":\"image\"}` objects both as top-level content blocks and as elements nested in `tool_result` content arrays. Counting rule: one image per image object, per record, in document order.",
"located": {
"claude_code": null,
"csift": "0.6.0",
"source": "SPEC.md section 6; src/image.rs:3-6 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "for L in 4351 10894; do csift show ~/.claude/projects/<enc>/<session>.jsonl --line $L --raw | jq -c '{type, role: .message.role, blocks: [.message.content[]? | {t: .type, nested: (if .type==\"tool_result\" then (if (.content|type)==\"array\" then [.content[]?.type] else (.content|type) end) else null end), src: (.source.type // null), mt: (.source.media_type // null)}]}'; done AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,80}type:\"base64\",media_type.{0,120}'",
"observed": "Direct carrier: `{\"type\":\"user\",\"role\":\"user\",\"blocks\":[{\"t\":\"text\"},{\"t\":\"image\",\"src\":\"base64\",\"mt\":\"image/jpeg\"}, ... 5 more image blocks]}`. Nested carrier, in the same transcript: `{\"type\":\"user\",\"role\":\"user\",\"blocks\":[{\"t\":\"tool_result\",\"nested\":[\"image\"]}]}` - a `tool_result` block whose `content` is an ARRAY holding an `image` element. Claude Code 2.1.258 binary emits exactly that second shape: `return{tool_use_id:n,type:\"tool_result\",content:[{type:\"image\",source:{type:\"base64\",media_type:o,data:r.data}}]}`. csift's JSON gave the direct-carrier record img_index 1..6 (a 1-based running ordinal) and the nested-carrier record img_index 1.",
"rule": "One image per `{type:\"image\"}` object, counted per record in document order across both carrier shapes; carrier shape read off the parsed block tree, not a text match.",
"note": "Code sites verified verbatim: src/image/refs.rs:231-234 (the doc comment) and src/image/refs.rs:255-259 (the `Block::ToolResult { content: Some(content), .. }` arm). The nested carrier is not hypothetical here - the three consecutive unnumbered images in the measured transcript (seq null, one per record) are all tool-result screenshots, and a listing that walked only direct blocks would have dropped them."
}
]
},
{
"id": "IMG-003",
"area": "images",
"behavior": "An image block's `source` carries a kind field; every image block observed on disk is `base64` (payload in `data`) plus a `media_type`, and the raw line always carries the `\"media_type\"` key in its compact form. The `url` kind is an Anthropic-API source shape that Claude Code 2.1.258 never writes: 0 image references out of 2977 in this corpus use it, and the binary contains no `type:\"url\"` construction. csift keeps the kind verbatim and would report a url-sourced image as having no bytes to extract, but that arm is defensive rather than exercised.",
"depends": "csift selects candidate lines with an OR of three needles - the compact pair `\"type\":\"image\"`, the key-only `\"media_type\"`, and the bare value substring `base64` (src/image/refs.rs:96-98, combined by `.any()` at line 101). The serialization tolerance comes from the UNION: the pair needle alone would miss a line reserialized with whitespace around the colon, but the key-only and bare-value needles both survive it.",
"code": [
{
"path": "src/image/refs.rs",
"lines": "28-36",
"snippet": " /// `\"base64\"` | `\"url\"` | another source kind (kept verbatim).\n pub(crate) source_kind: String,\n pub(crate) media_type: String,\n /// Base64 character length (0 for a non-base64 source).\n pub(crate) b64_len: usize,\n /// Estimated decoded byte size (base64 only; 0 otherwise).\n pub(crate) est_bytes: usize,\n /// The image URL, for a `source.type == \"url\"` image (no inline bytes to extract).\n pub(crate) url: Option<String>,"
},
{
"path": "src/image/refs.rs",
"lines": "95-99",
"snippet": " [\n memmem::Finder::new(br#\"\"type\":\"image\"\"#),\n memmem::Finder::new(br#\"\"media_type\"\"#),\n memmem::Finder::new(b\"base64\"),\n ]"
},
{
"path": "src/image/refs.rs",
"lines": "173-179",
"snippet": " let (b64_len, est_bytes, url, fingerprint, data) = if source_kind == \"url\" {\n let url = source\n .get(\"url\")\n .and_then(Value::as_str)\n .map(str::to_string);\n let fp = format!(\"url:{}\", url.as_deref().unwrap_or(\"\"));\n (0, 0, url, fp, None)"
}
],
"instrument": "`grep -c '\"media_type\"' ~/.claude/projects/*/*.jsonl` then `csift image @<session> --format json | jq -r '[.source_kind, .media_type, .b64_len] | @tsv'`. Counting rule: one reference per image block; a url source reports zero base64 length.",
"located": {
"claude_code": null,
"csift": "0.6.0",
"source": "AGENTS.md section 5; SPEC.md section 6"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for d in ~/.claude/projects/*/; do rg -oIN -g '*.jsonl' '\"media_type\"\\s*:\\s*\"[^\"]*\"' \"$d\"; done | sort | uniq -c AND csift image ~/.claude/projects/<enc> --format json | jq -r 'select(.kind==\"image\") | .source_kind' | sort | uniq -c AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'type:\"url\"' AND sed -n '93,101p' src/image/refs.rs # from the csift repo root",
"observed": "Corpus census over all 14 project dirs, *.jsonl only: 1748 `\"media_type\":\"image/png\"`, 1269 `\"media_type\":\"image/jpeg\"`, 4 `\"media_type\":\"application/pdf\"` - 3021 occurrences, every one the compact no-space form, and every image block carried the key. csift's source_kind census over the same corpus: base64 on 100% of image references (largest single project dir: 582 of 582), url on 0. The Claude Code 2.1.258 binary contains 0 occurrences of the string `type:\"url\"`. The csift prefilter at src/image/refs.rs:101 is `NEEDLES.iter().any(|f| f.find(line).is_some())` - an OR over the three needles at lines 96-98.",
"rule": "One media_type occurrence per regex match on a transcript line; one source_kind per csift image reference after content dedup; binary strings counted as occurrences of the literal.",
"note": "Two refinements. (1) The url arm is untestable against behavior here because Claude Code never produces it - the instrument that would decide it is a transcript written by a client that sends `{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":...}}`; note that such a block has no `media_type` under the Anthropic API, which would make the claim's 'the raw line always carries the media_type key' false for that shape. csift already defends: `media_type` falls back to `application/octet-stream` (src/image/refs.rs:168-172) and `ext()` to `bin`. (2) The claim's wording implies a key-only needle does the selecting; the code actually ORs a compact key:value pair in with two serialization-safe needles. All four claimed code sites exist verbatim (refs.rs:28-36, 95-99, 173-179)."
}
]
},
{
"id": "IMG-004",
"area": "images",
"behavior": "The `media_type` varies per image rather than always being `image/png`: this corpus carries `image/png` (1748) and `image/jpeg` (1269) side by side, and the Claude Code 2.1.258 binary carries the four accepted types `image/png`, `image/jpeg`, `image/gif`, `image/webp`. The non-standard `image/jpg` spelling is NOT observed in any transcript here (0 of 3021 media_type occurrences) and 2.1.258 actively normalizes it away when building a media type (`i===\"jpg\"?\"jpeg\":i`), so csift's jpg arm is defensive rather than a live shape.",
"depends": "csift derives each output file's extension from that image's own `media_type` (falling back to `bin`, never fabricated) and folds both jpeg spellings onto one output format; assuming PNG writes wrongly-named files, and a media type outside the mapped set must degrade rather than guess.",
"code": [
{
"path": "src/image/convert.rs",
"lines": "5-12",
"snippet": "/// A source `media_type` → an [`ImageOutFormat`]. `None` for a media type outside the four\n/// Claude-API image types (shouldn't occur - CC only stores those).\npub(crate) fn format_of_media_type(mt: &str) -> Option<ImageOutFormat> {\n match mt {\n \"image/png\" => Some(ImageOutFormat::Png),\n \"image/jpeg\" | \"image/jpg\" => Some(ImageOutFormat::Jpeg),\n \"image/gif\" => Some(ImageOutFormat::Gif),\n \"image/webp\" => Some(ImageOutFormat::Webp),"
},
{
"path": "src/image/refs.rs",
"lines": "59-66",
"snippet": " /// File extension implied by the media type (`bin` when unknown - never fabricated).\n pub(crate) fn ext(&self) -> &'static str {\n match self.media_type.as_str() {\n \"image/png\" => \"png\",\n \"image/jpeg\" | \"image/jpg\" => \"jpg\",\n \"image/gif\" => \"gif\",\n \"image/webp\" => \"webp\",\n \"image/svg+xml\" => \"svg\","
}
],
"instrument": "`csift image @<session> --format json | jq -r .media_type | sort | uniq -c`; expected: both jpeg spellings present in real data, one output extension. Counting rule: one media type per image block, after content dedup.",
"located": {
"claude_code": "2.1.237",
"csift": "0.3.0",
"source": "SPEC.md section 6.9; dev session 2026-08-17"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for d in ~/.claude/projects/*/; do rg -oIN -g '*.jsonl' '\"media_type\"\\s*:\\s*\"[^\"]*\"' \"$d\"; done | sort | uniq -c AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function C\\(e,i,r\\)\\{let n=i===\"jpg\"\\?\"jpeg\":i;.{0,120}' AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | sed -n '137076,137082p' AND csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id L42693i1,L42103i1 --out out && file out/*",
"observed": "Corpus census (*.jsonl, 14 project dirs): image/png 1748, image/jpeg 1269, image/jpg 0, image/gif 0, image/webp 0. Claude Code 2.1.258 NORMALIZES the jpg spelling away at construction time: `function C(e,i,r){let n=i===\"jpg\"?\"jpeg\":i;return{base64:e.toString(\"base64\"),mediaType:`image/${n}`,originalSize:r}}`. The binary's supported-image-type list is four adjacent strings `image/jpeg` / `image/png` / `image/gif` / `image/webp`; the literal `image/jpg` occurs 4 times in the binary, all outside that list (one bare occurrence sits in the browser-bridge string region). Extraction check: an `image/jpeg` source wrote `...-L42693i1.jpg` (450165 bytes, `file`: JPEG image data) and an `image/png` source wrote `...-L42103i1.png` (217338 bytes, `file`: PNG image data).",
"rule": "One media_type per regex match on a transcript line, whole-corpus, *.jsonl only; binary strings counted as occurrences of the literal; one output file per extracted image, extension read from the written filename.",
"note": "The claim's operative half - the extension must come from the image's own media_type, never assumed PNG - is confirmed by extraction: two images from one transcript wrote a .jpg and a .png, sized exactly as predicted. The `image/jpg` half is not reproducible at 2.1.258: 0 occurrences on disk and an explicit normalization in the binary. It would take a transcript written by an older Claude Code, or by the browser-bridge path where the bare `image/jpg` literal lives, to put that spelling on disk. Both code sites exist verbatim (src/image/convert.rs:5-12, src/image/refs.rs:59-66); note refs.rs `ext()` maps five more types beyond convert.rs's four (svg+xml, bmp, tiff, heic, avif) before the `bin` fallback."
}
]
},
{
"id": "IMG-005",
"area": "images",
"behavior": "Claude Code writes the model-facing image handle into the carrying record's own text as the literal marker `[Image #N]`, one marker per image block, in block order. A handle is unique within one prompt, but the counter does NOT restart at #1 on every prompt: measured across one transcript the distinct handles climb monotonically 1..624 over many prompts. The counter does reset at some coarser boundary - a second transcript shows `#1` naming 7 different images spread over four weeks - which is what makes a handle non-unique within a transcript (IMG-006).",
"depends": "csift recovers `#N` by positionally zipping a record's `[Image #N]` markers with its image blocks and leaves the number unset when the two counts differ (a mismatch means a back-reference to an image compacted out of the record), so addressing falls back to the always-unique `L<line>i<n>` locator; a change to the marker format loses the model-facing handle entirely.",
"code": [
{
"path": "src/image/refs.rs",
"lines": "216-221",
"snippet": "pub(crate) fn parse_image_markers(text: &str, out: &mut Vec<usize>) {\n let mut rest = text;\n const PAT: &str = \"[Image #\";\n while let Some(i) = rest.find(PAT) {\n let after = &rest[i + PAT.len()..];\n let digits: String = after.chars().take_while(char::is_ascii_digit).collect();"
},
{
"path": "src/image/refs.rs",
"lines": "275-282",
"snippet": " // Assign `#N` by POSITIONAL zip - only when the marker count matches the image count\n // (CC guarantees `[Image #N]` is unique within a prompt; a mismatch means a back-\n // reference to a compressed-out image, so we leave `seq = None` rather than misassign).\n if markers.len() == out.len() {\n for (r, &n) in out.iter_mut().zip(markers.iter()) {\n r.seq = Some(n);\n }\n }"
}
],
"instrument": "`rg -o '\\[Image #[0-9]+\\]' <transcript> | sort | uniq -c` for the raw markers against `csift image @<session> --format json | jq -c '{handle, seq, line}'`. Counting rule: one marker per regex match, one row per content-deduped image.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.0",
"source": "SPEC.md section 6; SPEC.md section 6 v0.6.2 ledger item 1; src/image/refs.rs:17-23 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -r 'select(.kind==\"image\")|[.line,.img_index,.seq,.b64_len]|@tsv' | sort -n AND csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id 1 AND rg -oIN '\\[Image #[0-9]+\\]' ~/.claude/projects/<enc>/<session2>.jsonl | sed 's/[^0-9]//g' | sort -n | uniq AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | sed -n '147155,147162p'",
"observed": "One record (line 4351) carries 6 image blocks; csift assigned them seq 1,2,3,4,5,6 by position, and the ambiguity listing shows that record's own text opening `[Image #1] [Image #2] [Image #3] [Image #4] [Image #5] [Im...` - the markers ride in the carrying record's text, one per block, in order. Across the same transcript's 191 deduped images, 188 got a seq and 3 stayed null (marker/block count mismatch), and the 3 nulls are exactly the tool-result screenshots. The 2.1.258 binary carries the literals `[Image #`, `[Image]`, `imageId`, and the error text `Cannot destructure property 'imageId' from null or undefined value`. Counter cadence: in a SECOND transcript the distinct handles run monotonically 1..624 across many prompts rather than restarting at #1 each prompt.",
"rule": "One marker per `\\[Image #[0-9]+\\]` regex match; one row per content-deduped image; seq assigned only when a record's marker count equals its image-block count.",
"note": "The positional-zip mechanism and the leave-unset-on-mismatch rule are both confirmed directly, and both code sites exist verbatim (src/image/refs.rs:216-221, 275-282). Only the phrase 'numbering pasted images PER PROMPT' needed correcting: per-prompt uniqueness holds, per-prompt restart does not."
}
]
},
{
"id": "IMG-006",
"area": "images",
"behavior": "Because the numbering restarts per prompt, low `[Image #N]` numbers are REUSED across prompts, so `#N` is not globally unique within one transcript and one number can name several distinct images.",
"depends": "csift resolves an `--id N` selection only when the number names ONE distinct image (distinctness by content fingerprint) and otherwise ERRORS with the full occurrence list - turn, `L<line>i<n>` locator, uuid, time and an excerpt around the marker - rather than silently picking one; the locator is the unambiguous fallback.",
"code": [
{
"path": "src/image/refs.rs",
"lines": "20-23",
"snippet": " /// `None` when the marker count doesn't match (then only `L<line>i<n>` addresses it).\n /// NOT globally unique - CC reuses low numbers across prompts, so a `#N` that names >1\n /// DISTINCT image is AMBIGUOUS: `--id #N` then ERRORS with the occurrence list rather than\n /// silently guessing (disambiguate with the locator or `--since`/`--turn`/`--uuid`)."
},
{
"path": "src/image/selection.rs",
"lines": "9-11",
"snippet": " /// `#N` / bare `N` - the `[Image #N]` handle the model uses. Resolves to the unique image\n /// with that handle in scope; if it names >1 DISTINCT image (CC reuses `#N` across prompts)\n /// it is AMBIGUOUS and ERRORS with the occurrence list rather than silently picking one."
},
{
"path": "src/image/selection.rs",
"lines": "281-286",
"snippet": " msg.push_str(&format!(\n \"--id #{n} is ambiguous: it names {} different images in this transcript (Claude Code \\\n reuses `#N` across prompts). Pick one by its exact `--id L<line>i<n>`, or narrow the \\\n scope with --since/--until (a time window) / --turn / --uuid:\",\n occs.len()\n ));"
}
],
"instrument": "`csift image @<session>` and look for one `#N` handle repeated on different lines; `rg -o '\\[Image #[0-9]+\\]' <transcript> | sort | uniq -c` shows the reuse. Counting rule: distinct images (by content fingerprint) per `#N` handle within one transcript.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.0",
"source": "SPEC.md section 6; SPEC.md section 6 v0.6.2 ledger item 1; src/image/refs.rs:17-23 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id 1 AND csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -r 'select(.kind==\"image\" and .seq==1)|[.line,.b64_len]|@tsv'",
"observed": "`--id 1` exits non-zero with `csift: error: --id #1 is ambiguous: it names 7 different images in this transcript (Claude Code reuses `#N` across prompts). Pick one by its exact `--id L<line>i<n>`, or narrow the scope with --since/--until (a time window) / --turn / --uuid:` followed by 7 occurrence lines, each with its own locator, turn, timestamp, uuid prefix and marker excerpt. The 7 are genuinely distinct content: base64 lengths 555588, 651860, 448616, 455720, 678200, 675456, 659632 on lines 4351, 13132, 25350, 47923, 56770, 83310, 94987, spanning turns t39 to t997 and four weeks of wall time.",
"rule": "Distinct images per `#N` handle within one transcript, distinctness by the csift content fingerprint (base64 length + head + tail); an ambiguous handle is one whose distinct count exceeds 1.",
"note": "The refusal path is real, not documentation: the command errors with the full occurrence list rather than picking one, and the locator disambiguates. All three code sites exist verbatim (src/image/refs.rs:20-23, src/image/selection.rs:9-11, 281-286). Note the counting rule matters here - a fingerprint taken from the base64 HEAD alone would wrongly merge these, see IMG-008."
}
]
},
{
"id": "IMG-007",
"area": "images",
"behavior": "A transcript's `[Image #N]` handles are not a dense 1..N index: the numbers are inherited from paste time, so the handles present carry HOLES where that number's image never landed in this transcript. Measured: 120 distinct handles over the range #1..#131 (11 holes) in one transcript, and holes throughout a #1..#624 range in another. The claim's other half - handles starting past #1 - was NOT reproduced here; both measured transcripts start at #1.",
"depends": "csift's `image --id` takes the bare handle number or the `L<line>i<n>` locator, and on a miss it names the handles that DO exist (plus the count of unnumbered images) and states that a missing number is a source gap, not a dropped image; a consumer that assumes a contiguous range asks for images that were never written.",
"code": [
{
"path": "src/image/selection.rs",
"lines": "154-156",
"snippet": " // Name the handles that DO exist: `#N` is inherited from CC's paste-time\n // `[Image #N]` numbering, so a transcript's handles can start past #1 and carry\n // holes - a bare \"matched no image\" reads like a csift drop when it is a source gap."
},
{
"path": "src/image/selection.rs",
"lines": "185-189",
"snippet": " \"--id matched no image: {} — {inventory}. `#N` handles are inherited from Claude \\\n Code's paste-time `[Image #N]` numbering, NOT a dense 1..N index csift assigns, \\\n so a transcript's handles can start past #1 and carry holes; a missing number is \\\n a source gap (that number's image never landed in this transcript), not a \\\n dropped image. Run `csift image` on the same target to see the full listing.\","
}
],
"instrument": "`csift image @<uuid> --format json | jq -r .seq | sort -n` against the dense 1..max range. Counting rule: distinct handles present versus the dense range; non-contiguous handles are expected.",
"located": {
"claude_code": null,
"csift": "0.6.2",
"source": "CHANGELOG 0.6.2; SKILL.md wrong-assumption row on image #N handles"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -oIN '\\[Image #[0-9]+\\]' ~/.claude/projects/<enc>/<session>.jsonl | sed 's/[^0-9]//g' | sort -n | uniq AND csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id 20 AND csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id 999",
"observed": "Transcript A: 120 distinct `[Image #N]` handles spanning #1..#131 - 11 numbers missing (#20, #103, #115-#120, #128-#130) out of the dense range. Transcript B: handles reach #624 with holes at #61, #121, #127, #129, #193-#194, #282-#283, #291, #341-#342, #358-#370, #507, #517, #520-#524, #537-#542, #546-#551, #555, #559, #568, #603-#604, #622. Asking for a hole (`--id 20`) and asking for an out-of-range number (`--id 999`) both produce the same non-zero error naming the handles that exist: `--id matched no image: #20 - present here: #1 #2 ... #25 (+95 more) + 3 unnumbered image(s) (address by the L<line>i<n> locator). `#N` handles are inherited from Claude Code's paste-time `[Image #N]` numbering, NOT a dense 1..N index csift assigns, so a transcript's handles can start past #1 and carry holes; a missing number is a source gap ...`. Both measured transcripts START at #1.",
"rule": "Distinct handle numbers present, from the deduplicated set of `\\[Image #[0-9]+\\]` matches in one transcript, compared against the dense 1..max range; a hole is a number in that range with no match.",
"note": "The holes half is confirmed twice over, and the error path names the present handles plus the count of unnumbered images exactly as claimed. The start-past-#1 half needs a transcript whose first pasted image already carried a high handle - plausible under the same inheritance mechanism, but not observed on this machine. Both code sites exist verbatim (src/image/selection.rs:154-156, 185-189)."
}
]
},
{
"id": "IMG-008",
"area": "images",
"behavior": "The same image is re-injected within one transcript - a prompt re-sends attached images and a compaction re-includes them - so one pasted image can appear as several separate image blocks. Measured, the effect is modest rather than large: 248 blocks over 196 distinct images in one transcript (1.27x, most-repeated image 4 copies) and 1164 blocks over 902 distinct in another (1.29x, most-repeated 9 copies). A raw block count therefore over-reports the image inventory by roughly a quarter, not by an order of magnitude.",
"depends": "csift's listing content-dedups by a cheap base64 fingerprint (`<len>:<head>:<tail>`) keeping the LATEST occurrence and its current `#N`, while two distinct-content images sharing one `#N` both survive so the handle reuse stays visible; a raw block count over-reports the image inventory.",
"code": [
{
"path": "src/image/selection.rs",
"lines": "57-60",
"snippet": "/// Dedup the SAME image re-injected across context windows (by content fingerprint), keeping\n/// the LATEST occurrence (its current `#N`). `images` is sorted ascending, so walking in\n/// reverse and keeping first-seen yields the latest; then order by `#N` (then line) for the\n/// listing so a reader scanning for \"#32\" finds it in sequence."
},
{
"path": "src/image/refs.rs",
"lines": "25-27",
"snippet": " /// A cheap content fingerprint (`<len>:<head>:<tail>` of the base64) - dedups the SAME\n /// image re-injected across context windows so the listing shows it once.\n pub(crate) fingerprint: String,"
}
],
"instrument": "Compare the deduped row count of `csift image @<session>` against `rg -c '\"media_type\"' <transcript>` (raw block count). Counting rule: distinct content fingerprints versus image blocks.",
"located": {
"claude_code": "2.1.258",
"csift": "0.3.0",
"source": "SPEC.md section 6.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -oIN '\"media_type\":\"image/[a-z]+\",\"data\":\"[A-Za-z0-9+/=]+\"' ~/.claude/projects/<enc>/<session>.jsonl | awk '{n=length($0); print n\":\"substr($0,1,60)\":\"substr($0,n-31,32)}' | sort | uniq -c AND csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -c 'select(.kind==\"summary\")' AND rg -oIN '\"media_type\":\"image/' <same file> | wc -l",
"observed": "Transcript A: 248 raw image blocks; the csift-shaped fingerprint (base64 length + 60-char head + 32-char tail) groups the 245 blocks it matched into 196 distinct images - multiplicity histogram 158 singles, 31 doubles, 3 triples, 4 quadruples, max 4 copies of any one image. csift's own deduped listing: 191 rows from 248 blocks. Transcript B: 1164 raw blocks, 1147 matched, 902 distinct - histogram 715/155/20/7/2/1/2 at multiplicities 1/2/3/4/5/6/9, max 9 copies; csift listing 897 rows from 1164 blocks. Adversarial control: fingerprinting on the base64 HEAD ALONE (first 48 chars) collapsed transcript A's 245 blocks to 170 groups and reported a bogus max multiplicity of 15 - same-dimension screenshots share a PNG prefix, so head-only merges distinct images.",
"rule": "Raw blocks counted one per `\"media_type\":\"image/` match in one transcript; distinct images counted one per (base64 length, head, tail) triple; multiplicity = raw blocks sharing one triple.",
"note": "The mechanism and its necessity both hold; only the magnitude ('many times') needed correcting to a measured ratio. The head-only control is the strongest support for the code as written: csift's `<len>:<head>:<tail>` form (src/image/refs.rs:25-27, src/image/selection.rs:57-60, both verbatim) is what keeps 26 distinct screenshots from collapsing into one row, and the two-distinct-images-sharing-#1 case survives dedup exactly as claimed (IMG-006 lists all 7 of that transcript's #1 images)."
}
]
},
{
"id": "IMG-009",
"area": "images",
"behavior": "Pasted images are ordinary rather than rare in an image-carrying session. Measured on this corpus's largest such transcript: 1164 image blocks spread over 393 records, 268 of them human-turn (`user.message`) records out of that file's 840 - about one human turn in three. The originally recorded 528 / 159 / 774 is a point-in-time snapshot of a transcript that has since grown; the direction it reported is now stronger, not weaker.",
"depends": "csift scans for images with the same mmap + byte-prefilter + parse-only-candidates path the other full-scan commands use and surfaces the malformed-line count, so the inventory is cheap to take on a large transcript; a text-only reader of the same session drops every image with no marker left behind.",
"code": [
{
"path": "src/image/refs.rs",
"lines": "299-302",
"snippet": "/// Scan ONE transcript for images. Mirrors `recover`/`files`: mmap + a pre-JSON byte\n/// prefilter + parse only candidate lines (line-numbered, 1:1 with the file). Returns the\n/// images and the malformed-line count (surfaced, never hidden).\npub(crate) fn images_in_file(path: &Path, with_data: bool) -> Result<(Vec<ImageRef>, usize)> {"
}
],
"instrument": "`csift image @<uuid> --format json | jq -c '{handle, seq, line, media_type}'`. Counting rule: 528 counted one per `{type:\"image\"}` block before content dedup; 159 of 774 counted one per human-turn record carrying at least one such block, in a single transcript.",
"located": {
"claude_code": null,
"csift": "0.6.2",
"source": "CHANGELOG 0.6.2; SKILL.md wrong-assumption row on image #N handles"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -r 'select(.kind==\"image\")|.line' | sort -nu > imglines.txt; csift search '' <same file> --no-subagents -t user.message --max-count 0 --format json | jq -r '.hits[]?|.line' | sort -nu > umlines.txt; rg -nI -o '\"media_type\":\"image/' <same file> | cut -d: -f1 | sort -nu > rawimglines.txt; comm -12 rawimglines.txt umlines.txt | wc -l AND time csift image <same file> --no-subagents --format json > /dev/null",
"observed": "In the largest image-carrying transcript in this corpus (720932882 bytes): 1164 raw image blocks over 393 distinct record lines; 268 of those 393 lines are `user.message` records, out of 840 `user.message` records in the file - 31.9%, about one human turn in three. csift scanned that 720MB file in 0.344s wall (0.33s user, 165% cpu) and reported `skipped_lines: 0` with 897 deduped images. A 415MB transcript scanned in 0.680s including its 12 subagent transcripts (265 images, skipped_lines 0). The claimed 528 / 159 / 774 triple does not match this or any other transcript here; the same file today reads 1164 / 268 / 840.",
"rule": "Image blocks counted one per `\"media_type\":\"image/` match; image-carrying human-turn records counted as the intersection of the distinct line numbers of those matches with the line numbers csift labels `user.message`; the denominator is that transcript's total `user.message` record count.",
"note": "The specific triple is not reproducible because transcripts are append-only and keep growing - a snapshot number in a ledger goes stale by construction, so the claim is better stated as a ratio with its counting rule. The performance half is confirmed independently: 0.344s for 720MB with an exact malformed-line census (0). The code site exists verbatim (src/image/refs.rs:299-302)."
}
]
},
{
"id": "IMG-010",
"area": "images",
"behavior": "Claude Code names its on-disk image cache files `<[Image #N] handle>.<media-type subtype>` under ~/.claude/image-cache/<session>/ - the extension is the media type's subtype taken verbatim, so an `image/jpeg` image is cached as `.jpeg` while csift extracts the same image to `.jpg`. The two names are attributes of different writers, not of different images. Note the subtype rule means a hypothetical `image/jpg` would cache as `.jpg`, not `.jpeg`.",
"depends": "csift maps both media types to one output format and writes `.jpg`, so a `.jpeg` file found beside a session is Claude Code's cache and not csift output; conflating the two attributes files to an extraction that never ran.",
"code": [
{
"path": "src/image/convert.rs",
"lines": "5-12",
"snippet": "/// A source `media_type` → an [`ImageOutFormat`]. `None` for a media type outside the four\n/// Claude-API image types (shouldn't occur - CC only stores those).\npub(crate) fn format_of_media_type(mt: &str) -> Option<ImageOutFormat> {\n match mt {\n \"image/png\" => Some(ImageOutFormat::Png),\n \"image/jpeg\" | \"image/jpg\" => Some(ImageOutFormat::Jpeg),\n \"image/gif\" => Some(ImageOutFormat::Gif),\n \"image/webp\" => Some(ImageOutFormat::Webp),"
},
{
"path": "src/image/refs.rs",
"lines": "59-66",
"snippet": " /// File extension implied by the media type (`bin` when unknown - never fabricated).\n pub(crate) fn ext(&self) -> &'static str {\n match self.media_type.as_str() {\n \"image/png\" => \"png\",\n \"image/jpeg\" | \"image/jpg\" => \"jpg\",\n \"image/gif\" => \"gif\",\n \"image/webp\" => \"webp\",\n \"image/svg+xml\" => \"svg\","
}
],
"instrument": "`csift image @<session> --format json | jq -r .media_type | sort | uniq -c` for the transcript side, then list the extensions Claude Code's image cache wrote for the same images. Counting rule: one media type per image block, one file per cached image.",
"located": {
"claude_code": "2.1.237",
"csift": "0.7.6",
"source": "dev session 2026-08-17"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,300}image-cache.{0,700}' AND find ~/.claude -type f \\( -iname '*.jpeg' -o -iname '*.jpg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.webp' \\) | sed 's/.*\\.//' | sort | uniq -c AND ls -l ~/.claude/image-cache/*/ AND csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -c 'select(.kind==\"image\")|{seq,est_bytes,media_type}' AND csift image <same file> --no-subagents --id L42693i1 --out out && file out/*",
"observed": "The 2.1.258 binary builds the cache path as `var g=\"image-cache\", O=200; function f(){return d(Se(),g,Q())} function p(e,r){let o=r.split(\"/\")[1]||\"png\";return d(f(),`${e}.${o}`)}` and calls it as `p(e.id, e.mediaType||\"image/png\")` - so the extension is the media type's SUBTYPE verbatim: `image/jpeg` yields `.jpeg`, and `image/jpg` would yield `.jpg`, not `.jpeg`. On disk today ~/.claude/image-cache holds 7 files across 2 sessions, all `.png`; across all of ~/.claude there are 596 image files (581 .jpg, 14 .png, 1 .gif) and ZERO `.jpeg`. The cache basename is the `[Image #N]` handle: the file `9.png` is 328603 bytes and that session's transcript image `#9` decodes to exactly 328603 bytes. csift on the other side wrote `...-L42693i1.jpg` for an `image/jpeg` source (450165 bytes, `file`: JPEG image data). The only `.jpeg` literal elsewhere in the binary is `public.jpeg`, an operating-system pasteboard type identifier listed beside `public.png` / `public.heic` / `public.avif`, not a filename extension.",
"rule": "Cache filename rule read off the binary's path builder, then checked against the on-disk cache; extensions counted one per file under ~/.claude; media types counted one per csift image reference; the handle-to-file join is byte-size equality between the cached file and the transcript image's decoded length.",
"note": "The `.jpeg`-vs-`.jpg` divergence is confirmed as a rule but not as a live artifact: no `.jpeg` file exists under ~/.claude right now because both cached sessions happened to paste PNG-sourced images and the cache is pruned (the binary carries `Cleaned up old image cache:` and a cap constant of 200). Deciding it as an artifact needs a jpeg-sourced paste in a session whose cache has not yet been pruned. Both code sites exist verbatim (src/image/convert.rs:5-12, src/image/refs.rs:59-66), and the extraction check confirms csift writes `.jpg` for `image/jpeg`. code-site note: The mechanism half of this claim now has a binary receipt as well: `var g=\"image-cache\"` with `function p(e,r){let o=r.split(\"/\")[1]||\"png\";return d(f(),`${e}.${o}`)}` in Claude Code 2.1.258."
}
]
},
{
"id": "IMG-011",
"area": "images",
"behavior": "A `Read` of an image file writes a `toolUseResult` whose top-level `type` is `image` and whose `file` object has NO `filePath` at all: it carries `base64` (the payload), `type` (the media type - `image/png` and `image/jpeg` are what the corpus holds), `originalSize`, and an OPTIONAL `dimensions:{originalWidth, originalHeight, displayWidth, displayHeight}` present on 435 of 444 such results.",
"depends": "csift's `recover` Read arm keys on a `file` object with a matching `filePath` and calls `path_matches(target_file, path.unwrap_or_default())`, so an image Read reaches that arm with an EMPTY path string; `path_matches` returns false on an empty target match, which is the only reason an image Read is never mistaken for a file event - the `unwrap_or_default()` is load-bearing, not incidental.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "56-58",
"snippet": " if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {"
},
{
"path": "src/recover/events.rs",
"lines": "173-177",
"snippet": "pub(crate) fn path_matches(target: Option<&str>, path: &str) -> bool {\n let Some(t) = target else { return false };\n if t == path {\n return true;\n }"
}
],
"instrument": "Measured 2026-09-02 over the 7,814 transcript files under ~/.claude/projects with a python3 walk that json-parses every line carrying `toolUseResult` and keeps the ones whose `toolUseResult.file` is an object, bucketing `Counter(tuple(sorted(file.keys())))` by the top-level `type`. Counting rule: one observation per record with an object `file`. Result: 444 `image` results; `file.filePath` present on 0 of 444; key set `{base64, dimensions, originalSize, type}` on 435 and `{base64, originalSize, type}` on 9; `file.type` values `image/png` 371 and `image/jpeg` 73. Schema authority: the Read tool-result schema in the Claude Code 2.1.258 binary, whose `image` variant declares exactly those four members.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) python3 -c \"import json,os,collections; R=os.path.expanduser('~/.claude/projects'); C=collections.Counter(); T=collections.Counter(); n=0; fp=0; [ (n:=n+1, C.update([tuple(sorted(t['file']))]), T.update([t['file'].get('type')])) for dp,_,fs in os.walk(R) for f in fs if f.endswith('.jsonl') for l in open(os.path.join(dp,f),errors='replace') if 'toolUseResult' in l for r in [json.loads(l)] for t in [r.get('toolUseResult')] if isinstance(t,dict) and isinstance(t.get('file'),dict) and t.get('type')=='image' ]\" (run as the multi-line equivalent; walks every *.jsonl under ~/.claude/projects, json-parses each line containing toolUseResult, keeps records whose toolUseResult.file is an object, buckets by top-level type) (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'c\\(\\{type:x\\(\"image\"\\).{300}' (c) csift recover --file <an image file read in that session> @<session> --no-subagents --coverage",
"observed": "(a) 7829 transcript files scanned; toolUseResult types carrying an object `file`: text 8609, image 444, file_unchanged 120, parts 26, pdf 4. image results = 444; `file.filePath` present on 0 of 444; file key sets {base64,dimensions,originalSize,type} on 435 and {base64,originalSize,type} on 9; file.type = image/png 371, image/jpeg 73. (b) verbatim binary string: 'c({type:x(\"image\"),file:c({base64:i().describe(\"Base64-encoded image data\"),type:e.describe(\"The MIME type of the image\"),originalSize:A().describe(\"Original file size in bytes\"),dimensions:c({originalWidth:A().optional()...displayHeight:A().optional().describe(\"Displayed image height in pixels (after resizing)\")}).optional().describe(\"Image dimension info for coordinate mapping\")})})' - exactly the four members base64/type/originalSize/dimensions, dimensions optional, no filePath declared. (c) csift printed 'no recoverable history for <that image file> in range' (exit 0) even though the Read of it is on the transcript (tool_use line 558, result line 561).",
"rule": "One observation per record whose toolUseResult.file is an object, counted once per file on disk (the corpus holds duplicated project directories, so 444 observations resolve to 315 distinct record uuids; every ratio quoted here is observation-level and the dedup does not change any of them qualitatively - filePath is absent on 0 under either rule). The recover check counts anchors minted for a --file target: 0 read events for an image Read, versus 1 for the PDF case of IMG-012.",
"note": "Reproduces exactly, at both instruments. The binary schema is the stronger form of the claim: filePath is not merely absent from the corpus, it is not a declared member of the image variant. Two secondary observations worth carrying: (1) 444 file-level observations are 315 distinct record uuids, because two project directories hold copies of the same session; (2) image/png and image/jpeg are a corpus fact, not a schema limit - Claude Code's media-type maps in the same binary also carry image/gif and image/webp, so a future corpus can hold those. The depends clause is confirmed behaviourally, not just by reading: recover finds no history for a file whose only Read produced an image result."
}
]
},
{
"id": "IMG-012",
"area": "images",
"behavior": "A `Read` of a PDF that Claude Code splits into page images writes a `toolUseResult` whose top-level `type` is `parts` and whose `file` object is exactly `{count, filePath, originalSize, outputDir}` - it NAMES an on-disk output directory instead of carrying the pages in `file`; a top-level `firstPage` rides beside it only on results written by Claude Code 2.1.251 and later (2 of 26 observed, and both of those are the 2.1.251 records).",
"depends": "csift's `recover` Read arm keys on the presence of a `file` object with a matching `filePath`, and a `parts` result carries one (the PDF path), so it enters the same path as a text Read but supplies no `content` (the `unwrap_or_default()` yields an empty string). The `total > 0` gate in `push_read_event` is what stops that contentless variant from being minted as a FULL snapshot: the event still lands, on the `else` arm, as a zero-line windowed read, so `recover --file <the pdf>` reports `0/1 lines (0%)` with `1 read (0 full, 1 windowed)` rather than a false full read.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "59-63",
"snippet": " let content = file\n .get(\"content\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default()\n .to_string();"
},
{
"path": "src/recover/carriers.rs",
"lines": "72-77",
"snippet": " let num_lines = file\n .get(\"numLines\")\n .and_then(serde_json::Value::as_u64)\n .map(|n| n as usize);\n push_read_event(\n line_no,"
},
{
"path": "src/recover/carriers.rs",
"lines": "178-181",
"snippet": " let lines: Vec<String> = split_lines(content);\n let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;"
}
],
"instrument": "Measured 2026-09-02 over the 7,814 transcript files under ~/.claude/projects with a python3 walk that json-parses every line carrying `toolUseResult` and keeps the ones whose `toolUseResult.file` is an object, filtered to `type == 'parts'` and tallying both the `file` key set and the top-level key set. Counting rule: one observation per record with an object `file`. Result: 26 `parts` results; `file` key set exactly `{count, filePath, originalSize, outputDir}` on 26 of 26; top-level key set `{file, type}` on 24 and `{file, firstPage, type}` on 2.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) python3 walk over ~/.claude/projects filtered to toolUseResult.type == 'parts', tallying the `file` key set, the top-level key set, and the count of {type:\"image\"} elements nested in the record's tool_result blocks (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'c\\(\\{type:x\\(\"parts\"\\).{700}' (c) csift recover --file <the PDF> @<session> --coverage",
"observed": "(a) 26 parts results; file key set exactly {count, filePath, originalSize, outputDir} on 26 of 26; top-level key set {file, type} on 24 and {file, firstPage, type} on 2. (b) verbatim binary string: 'c({type:x(\"parts\"),file:c({filePath:i().describe(\"The path to the PDF file\"),originalSize:A().describe(\"Original file size in bytes\"),count:A().describe(\"Number of pages extracted\"),outputDir:i().describe(\"Directory containing extracted page images\")}),firstPage:A().optional().describe(\"Document page number of the first extracted page (1 when no range was requested); labels the page images in the model-facing tool_result\")' - exactly those four file members, firstPage optional. (c) csift printed 'recoverable: 0/1 lines (0%) fragments: 1' and 'events: 1 read (0 full, 1 windowed)', exit 0 - one event was minted, none of it recoverable.",
"rule": "One observation per record with an object `file` and type 'parts', over every *.jsonl under ~/.claude/projects (26 observations, 26 distinct record uuids - no duplication in this bucket). The recover check counts events by kind as csift's own --coverage line reports them.",
"note": "The measured numbers reproduce exactly and the binary schema confirms the key set. Two corrections. (1) The depends clause overstated the gate: recover does NOT record nothing - it records one windowed read event with zero recoverable lines. What the `total > 0` gate prevents is the FullSnapshot arm, i.e. a false full read, which is the load-bearing half of the claim; the event itself is still created on the else arm. (2) `firstPage` is not random: the 2 results carrying it are exactly the 2 written by Claude Code 2.1.251, and the 24 without it were written by 2.1.196 / 2.1.207 / 2.1.220 / 2.1.231, so the correct reading is a version boundary rather than an intermittent field. Also noted, not a correction: the production site in the binary can attach a top-level `v5SidecarScope:{...,pageNames}` ('return{success:!0,data:{type:\"parts\",file:{filePath:e,originalSize:s,outputDir:b,count:B},...d!==void 0&&{v5SidecarScope:{...d,pageNames:_}}}}'), but the persisted union does not declare that member and it appears on 0 of 26 records."
}
]
},
{
"id": "IMG-013",
"area": "images",
"behavior": "An image `Read` stores the payload TWICE inside the one jsonl line: once as the `toolUseResult.file.base64` echo and once as an `{type:\"image\"}` element nested in the record's `tool_result` content array. Every one of the 444 observed image results carries exactly one such nested element, the element's `source.media_type` equals the echo's `file.type` on 444 of 444, and the two base64 strings are byte-identical on 435 of 444.",
"depends": "csift's `image` walks `rec.blocks()` - the message content - and never reads `toolUseResult` (kept unparsed), so it counts the nested element once and the echo copy never double-counts a row; a hand-rolled census over raw `base64` bytes counts one image Read twice, and a byte-size estimate taken over the raw line doubles the real payload.",
"code": [
{
"path": "src/image/refs.rs",
"lines": "235-239",
"snippet": "pub(crate) fn record_images(rec: &Record, with_data: bool) -> Vec<ImageRef> {\n let mut out = Vec::new();\n let Some(blocks) = rec.blocks() else {\n return out;\n };"
},
{
"path": "src/image/refs.rs",
"lines": "255-259",
"snippet": " Block::ToolResult {\n content: Some(content),\n ..\n } => {\n if let Some(arr) = content.as_array() {"
}
],
"instrument": "python3 over ~/.claude/projects: for every record whose `toolUseResult.type` is `image`, walk `message.content` for `{type:\"image\"}` elements nested in `tool_result` blocks and compare each element's `source.data` with `toolUseResult.file.base64`. Counting rule: one observation per record, with the nested-element tally taken per record. Measured 2026-09-02: 444 records; nested-element count 1 on 444 of 444; `source.media_type == file.type` on 444 of 444; base64 byte-identical on 435 of 444.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 -c \"...\" walk over ~/.claude/projects: for every record whose toolUseResult.type is 'image', collect the {type:\"image\"} elements nested in the record's tool_result content arrays, then compare each element's source.media_type with toolUseResult.file.type and its source.data with toolUseResult.file.base64",
"observed": "444 image records; nested-element count 1 on 444 of 444 (no record with 0 or 2+); source.media_type == file.type on 444 of 444; base64 byte-identical on 435 of 444; the 9 non-identical pairs are all (echo length 0, block length 118908/118908/129212/129212/130588/138368/141016/153900/153900).",
"rule": "One observation per record with an object `file` and type 'image'; the nested-element tally is taken per record, over all *.jsonl under ~/.claude/projects. 444 observations resolve to 315 distinct record uuids (duplicated project directories); the per-record ratios are unchanged by the dedup because the duplicates are exact copies.",
"note": "Reproduces exactly, every number. The double-storage is real and current: the payload sits both in the toolUseResult echo and in one nested content block, so a raw-bytes census over the line double-counts an image Read and a byte-size estimate over the raw line doubles the payload. csift's `image` walks rec.blocks() only, and the confirming instrument is IMG-014's csift run: it emits exactly one row per image record (id L<line>i1), never two."
}
]
},
{
"id": "IMG-014",
"area": "images",
"behavior": "The echo copy is not a reliable carrier: on 9 of 444 image `Read` results the `toolUseResult.file.base64` is an EMPTY string while the nested content block still holds the full payload (118,908 to 153,900 base64 characters across those 9), so the content block is the authoritative source of the bytes.",
"depends": "csift's `image` reads the block's `source.data` and returns no reference at all when that key is missing, so `image --out` writes real bytes on exactly those records; a tool that preferred the `toolUseResult` echo would write a zero-byte file and report success, and a size report taken from the echo would read 0 for a 100KB image.",
"code": [
{
"path": "src/image/refs.rs",
"lines": "181-184",
"snippet": " let d = source.get(\"data\").and_then(Value::as_str)?;\n // Cheap content fingerprint: length + base64 head/tail. Dedups the SAME image\n // re-injected across context windows without holding/hashing the full payload.\n let head: String = d.chars().take(32).collect();"
}
],
"instrument": "The pairing census of the previous claim, filtered to the records where the echo and the nested block disagree. Counting rule: one observation per record. Measured 2026-09-02: 9 of 444 disagree, and in all 9 the echo string has length 0 while the block's `source.data` measures 118,908-153,900 characters. Cross-check: `csift image @<session> --format json | jq -r .b64_len` reports the block's length on those rows, never 0.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) the IMG-013 pairing census, filtered to the records where the echo and the nested block disagree (b) csift image @<session> --no-subagents --format json | (extract line, media_type, b64_len) for the two sessions holding those records",
"observed": "(a) 9 of 444 disagree; in all 9 the echo string has length 0 while the nested block's source.data measures 118,908 / 129,212 / 130,588 / 138,368 / 141,016 / 153,900 characters. (b) csift reported b64_len 130588, 138368, 141016 (three rows, one per record, ids L<line>i1) in one session and 153900, 118908, 129212 in the other - six rows, none 0, matching the census lengths one for one.",
"rule": "One observation per record with an object `file` and type 'image'. The 9 disagreeing observations are 6 distinct record uuids (three of them appear in two copies of the same session under two project directories); the csift cross-check enumerates the 6 distinct records, one row each.",
"note": "Reproduces exactly, and the csift cross-check the claim proposes was actually run: on every record whose toolUseResult.file.base64 is the empty string, csift's b64_len is the nested block's length, never 0. Counting-rule footnote for a stranger rerunning it: '9 of 444' is the file-level observation count and the corpus duplicates one session across two project directories, so the distinct-record form of the same fact is '6 of 315'. Either way, the direction of the claim is unchanged - the echo is unreliable, the content block is authoritative."
}
]
},
{
"id": "IMG-015",
"area": "images",
"behavior": "The `pages` key is stripped before a `parts` Read result is persisted - 0 of 26 observed records carry it at the top level or inside `file`, and the 2.1.258 schema documents it as in-process only, 'not retained on the tool_use_result, so this key is absent on the emitted/persisted result'. That is a fact about the KEY, not about the bytes. Since 2.1.251 the page images DO reach the transcript, delivered in the persisted tool_result CONTENT as one `{type:\"image\"}` block per extracted page (block count equals `file.count`, bytes identical to the `page-N.jpg` files in the recorded `outputDir`). On records written by 2.1.196 through 2.1.231 that same content is instead the plain string `PDF pages extracted: N page(s) from <path>`, with no image blocks anywhere, and the output directory is the only copy.",
"depends": "csift's `image` scans inline base64 in BOTH a direct image block and an `{type:\"image\"}` element nested in a tool_result content array, so on a 2.1.251-or-later `parts` Read the pages are surfaced as ordinary `L<line>i<n>` rows and written out by `image --out` - a PDF page is recoverable from the transcript alone, no csift change required. On an older record there is nothing inline, `image` correctly reports nothing, and the recorded output directory is the only pointer: the invisibility is a version boundary, not a permanent source fact. `recover --file <the pdf>` has no text content to replay in either era, since the `parts` `file` object carries no `content` string.",
"code": [
{
"path": "src/image/refs.rs",
"lines": "299-302",
"snippet": "/// Scan ONE transcript for images. Mirrors `recover`/`files`: mmap + a pre-JSON byte\n/// prefilter + parse only candidate lines (line-numbered, 1:1 with the file). Returns the\n/// images and the malformed-line count (surfaced, never hidden).\npub(crate) fn images_in_file(path: &Path, with_data: bool) -> Result<(Vec<ImageRef>, usize)> {"
},
{
"path": "src/image/refs.rs",
"lines": "231-234",
"snippet": "/// Collect every image carried by one record, in document order: a direct `Block::Image`\n/// (the user-sent / assistant case) OR an `{type:\"image\"}` element nested in a\n/// `Block::ToolResult.content` array (a tool screenshot). `img_index` is a 1-based running\n/// ordinal across both sources, so the id `L<line>i<n>` is stable."
},
{
"path": "src/image/refs.rs",
"lines": "255-266",
"snippet": " Block::ToolResult {\n content: Some(content),\n ..\n } => {\n if let Some(arr) = content.as_array() {\n for el in arr {\n if el.get(\"type\").and_then(Value::as_str) == Some(\"image\") {\n if let Some(src) = el.get(\"source\") {\n if let Some(mut r) = image_ref_from_source(src, with_data) {\n r.img_index = out.len() + 1;\n out.push(r);\n }"
}
],
"instrument": "Measured 2026-09-02 over the 7,814 transcript files under ~/.claude/projects with a python3 walk that json-parses every line carrying `toolUseResult` and keeps the ones whose `toolUseResult.file` is an object, filtered to `type == 'parts'` and testing for a `pages` key at the top level and inside `file`. Counting rule: one observation per record. Result: 0 of 26 carry `pages` at either level. Schema authority: the Read tool-result schema in the Claude Code 2.1.258 binary documents the page images as not retained on the persisted tool result, and the result is stripped of that key before it is written.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "(a) python3 walk over ~/.claude/projects filtered to toolUseResult.type == 'parts', testing for a `pages` key at the top level and inside `file`, and counting the {type:\"image\"} elements nested in the record's tool_result content, bucketed by the record's `version` field (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'pages:R\\(c\\(\\{base64:i\\(\\).{600}' (c) csift image @<session> --no-subagents --format json, filtered to the jsonl line of a 2.1.251 parts record (d) ls -la on the outputDir that same record names",
"observed": "(a) 26 parts results; `pages` present at top level on 0 and inside `file` on 0. Nested {type:\"image\"} elements per parts record: 0 on 24 records, 5 on one, 3 on one. Split by writer version: the two records WITH nested image blocks were written by 2.1.251 (and are the two carrying top-level firstPage); the 24 with none were written by 2.1.196 (1), 2.1.207 (2), 2.1.220 (15), 2.1.231 (6), and on those the tool_result content is not an array at all but the plain string 'PDF pages extracted: N page(s) from <path>'. On both 2.1.251 records the nested image count equals file.count. (b) verbatim binary string: 'pages:R(c({base64:i().describe(\"Base64-encoded page image; empty when the page could not be processed\"),mediaType:e.describe(\"The MIME type of the image\"),error:i().optional()...})).optional().describe(\"Extracted page images, in page order. Present only transiently in-process: the page image bytes are delivered solely as image blocks in the model-facing tool_result content and are not retained on the tool_use_result, so this key is absent on the emitted/persisted result\")'. (c) csift emitted five rows for that one jsonl line - L<line>i1..i5, media_type image/jpeg, b64_len 58928 / 124844 / 111072 / 125080 / 125124. (d) the named outputDir holds page-1.jpg .. page-5.jpg, 44,196 / 93,631 / 83,303 / 93,810 / 93,841 bytes; 44,196 bytes base64-encodes to exactly 58,928 characters, matching csift's first row.",
"rule": "One observation per record with an object `file` and type 'parts' (26 observations, 26 distinct record uuids). Nested-image count is per record; the version split is the record's own `version` field. The csift count is rows emitted for a single jsonl line.",
"note": "The literal `pages`-key half of the claim still holds - 0 of 26 records carry it at either level, and the 2.1.258 schema documents that key as in-process only and 'absent on the emitted/persisted result'. But the conclusion drawn from it no longer describes current Claude Code, and it was the operative half. NEW BEHAVIOR: since 2.1.251 the page bytes DO reach the transcript. The persisted tool_result content for a parts Read is now an array carrying one {type:\"image\"} block per extracted page (5 and 3 blocks, each equal to file.count) plus one text element, and those blocks are byte-for-byte the page-N.jpg files in the outputDir. On every record written by 2.1.196 through 2.1.231 the same content is a plain string, 'PDF pages extracted: N page(s) from <path>', with no image blocks anywhere - that is the shape the claim was written against. Consequences for csift: the pages are NOT invisible to `image` any more - `csift image` surfaces them as ordinary L<line>i<n> rows and `image --out` will write them, so a PDF page is now recoverable from the transcript alone, and the claim's 'the absence is a source fact, not a csift miss' is false on current data. What is still true: outputDir remains the on-disk copy, and `recover --file <the pdf>` still has no text content to replay (see IMG-012). The claim should be rewritten as a version-boundary fact rather than a permanent absence."
}
]
},
{
"id": "REG-001",
"area": "live-registry",
"behavior": "Claude Code keeps a session registry under its home directory, one JSON row per live top-level session at `<claude-home>/sessions/<pid>.json`. The observed key set is `pid`, `sessionId`, `cwd`, `startedAt` (millisecond epoch), `procStart` (the owner process's creation instant), `version`, `kind`, `entrypoint`, `status`, `updatedAt` and `statusUpdatedAt`, plus row-dependent `peerProtocol`, `messagingSocketPath`, `name`, `nameSince`, `nameSource`, `peerFeatures`, `pidDomain`, `bridgeSessionId` and `tmux`. `kind` is a closed four-value set (`interactive` | `bg` | `daemon` | `daemon-worker`), of which only `interactive` occurs on an interactive host; `entrypoint` is NOT a closed pair - the writer copies `$CLAUDE_CODE_ENTRYPOINT` through verbatim and the reader accepts any string, so `cli` and `sdk-cli` are two members of an open set. The row is keyed on disk by pid, and the session it describes is named by the `sessionId` field, not by the file name.",
"depends": "`csift status` and `csift wait` join this row as one of three verdict surfaces, looking it up by scanning the directory for the matching `sessionId` and reading `pid`, `status`, `statusUpdatedAt`, `procStart` and `pidDomain`; a renamed key silently drops the registry leg out of the join and every verdict degrades to the no-registry lane.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "1-5",
"snippet": "//! The harness session registry: `<claude-home>/sessions/<pid>.json`.\n//!\n//! Field shape (verified on disk, CC 2.1.258): `{pid, sessionId, cwd, startedAt(ms),\n//! procStart(str), version, kind:\"interactive\", entrypoint:\"cli\"|\"sdk-cli\", pidDomain,\n//! status, updatedAt, statusUpdatedAt, bridgeSessionId?, tmux?, ...}`. `status` is the"
},
{
"path": "src/live/registry.rs",
"lines": "40-44",
"snippet": "pub(crate) fn registry_row_for(session_id: &str) -> Result<Option<RegistryRow>> {\n let dir = crate::path::claude_home()?.join(\"sessions\");\n if !dir.is_dir() {\n return Ok(None);\n }"
},
{
"path": "src/live/registry.rs",
"lines": "57-59",
"snippet": " if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) {\n continue;\n }"
}
],
"instrument": "`python3 -c \"import json,glob,pathlib;[print(sorted(json.load(open(p)))) for p in glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json'))]\"` - every row prints a superset of {pid, sessionId, cwd, startedAt, procStart, version, kind, entrypoint, status, updatedAt, statusUpdatedAt}; then `csift status @<session> --format json | jq '.evidence[] | select(.surface==\"registry\")'` shows the row csift joined. Counting rule: one observation per `.json` file in that directory (non-`.json` sidecars excluded by extension), at one instant - the directory is current state, not history.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "SPEC.md section 6.13; AGENTS.md section 1; CHANGELOG 0.9.0; src/live/registry.rs module doc; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import json,glob,os,pathlib;[print(os.path.basename(p),json.load(open(p)).get('pid'),sorted(json.load(open(p)))) for p in sorted(glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json')))]\" AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'pid:process.pid,sessionId:.{0,220}' AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var .{1,3}=\\[\"interactive\",\"bg\",\"daemon\",\"daemon-worker\"\\]'",
"observed": "7 rows at the first sampling instant (10 rows 40 min later, as short-lived sessions registered and exited). All 7/7 carry a superset of {pid, sessionId, cwd, startedAt, procStart, version, kind, entrypoint, status, updatedAt, statusUpdatedAt}; the extras seen are exactly {bridgeSessionId, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pidDomain}; `tmux` appeared on 0/7. File-name stem == the row's own `pid` field on 7/7. `sessionId` matched the 36-char uuid shape on 7/7 (and 8/8, 10/10 at later instants); `startedAt` was a 13-digit int on 7/7. Value census: kind=interactive 7/7; entrypoint=cli 7/7; version spread over six binary generations; pidDomain=darwin on 4/7 and absent on 3/7 (older rows). Binary writer, verbatim: `S({pid:process.pid,sessionId:Q(),cwd:ye(),startedAt:Date.now(),procStart:await ja(process.pid),version:{...}.VERSION,peerProtocol:Fzt,peerFeatures:Pwr(),kind:o,entrypoint:a.CLAUDE_CODE_ENTRYPOINT,pidDomain:await YG(),...C&&{tmux:C},...{messagingSocketPath:...},...{name:I?.name,nameSource:...,nameSince:Date.now(),logPath:...,agent:...,jobId:...,spare:...}})` written to `Ct(_,`${process.pid}.json`)`; the directory reader filters `/^\\d+\\.json$/`. `kind` is validated by a closed set: `var $e=[\"interactive\",\"bg\",\"daemon\",\"daemon-worker\"];function Ge(e){return $e.includes(e)?e:void 0}`.",
"rule": "One observation per `*.json` file in ~/.claude/sessions at one instant (the `<pid>.<64hex>.key` messaging-auth sidecars in the same directory are excluded by extension). A key counts as present if it appears at the JSON top level; a value census counts one value per file. Binary side: one quoted string per pattern from `strings -n 6` over the 2.1.258 binary.",
"note": "Holds as an on-disk observation; refined because the claim's parenthetical value sets read as closed and only one of them is. `kind` really is closed (a 4-value validator in the binary), `entrypoint` is not (verbatim env passthrough; the binary's own entrypoint switch also names claude-vscode, remote*, sdk-ts, sdk-py, mcp, local-agent and more). `tmux` was on 0/7 rows here, which is consistent with the claim calling it row-dependent, not a refutation. csift code sites confirmed verbatim: src/live/registry.rs lines 1-5, 40-44 and 57-59 are unchanged."
}
]
},
{
"id": "REG-002",
"area": "live-registry",
"behavior": "The registry row's `status` is the binary's closed four-value set `busy | shell | idle | waiting`, byte-identical across every binary generation on this machine (2.1.229 through 2.1.258). `waiting` is written whenever a dialog blocks the session - a queued elicitation (`input needed`), the top dialog's own label, a worker request, a sandbox request, or an open local command dialog - with the reason mirrored into a companion `waitingFor` string field on the same row; `busy` covers loading and delegating; `idle` is the default. On disk `idle`, `busy` and `shell` were observed. The set is the harness's, not csift's, and must be treated as extensible.",
"depends": "`csift status` maps registry `waiting` onto its blocked-on-human verdict and ranks the remaining values for running-shape; a new running-ish value csift does not list would make an actively working session read as idle at end of turn and satisfy `wait --until stop` early.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "5-9",
"snippet": "//! status, updatedAt, statusUpdatedAt, bridgeSessionId?, tmux?, ...}`. `status` is the\n//! binary's closed set `busy | shell | idle | waiting`: `waiting` whenever the session is\n//! blocked on a dialog (a question, a permission prompt, a plan approval, a sandbox or\n//! worker request), `busy` while loading or delegating, else `idle`. A print-mode\n//! (`claude -p`) session writes a row too, with `status: null` that never transitions."
},
{
"path": "src/live/verdict.rs",
"lines": "291-298",
"snippet": " // Two HITL legs beside the sidecar: the registry's `waiting` status (the binary sets\n // it whenever a dialog blocks the session: a question, a permission prompt, a plan\n // approval, a sandbox/worker request), and an unreturned AskUserQuestion/ExitPlanMode\n // at the tail (a MULTI-question ask is written at question time since CC 2.1.258;\n // a single-question ask stays buffered until answered - the sidecar's shape).\n let registry_waiting = registry\n .and_then(|r| r.status.as_deref())\n .is_some_and(|s| s == \"waiting\");"
}
],
"instrument": "`python3 -c \"import json,glob,pathlib,collections;print(collections.Counter(json.load(open(p)).get('status') for p in glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json'))))\"` while several sessions run, and repeat while one sits on a permission prompt to catch `waiting`. Counting rule: one status observation per registry file per sampling instant; the file is point-in-time, so a value seen zero times is not evidence it cannot occur.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "SPEC.md section 6.13; src/live/registry.rs module doc; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var .{1,3}=\\[\"busy\",\"shell\",\"idle\",\"waiting\"\\];function .{1,3}\\(e\\)\\{return .{1,3}\\.includes\\(e\\)\\?e:void 0\\}' AND rg -o 'function XBe\\(T\\)\\{let I=o_o\\(T\\).{0,320}' AND for v in 2.1.229 2.1.234 2.1.241 2.1.251 2.1.257 2.1.258; do strings -n 6 ~/.local/share/claude/versions/$v | rg -o '\\[\"busy\",[^]]{0,80}\\]' | sort -u; done AND python3 -c \"import json,glob,pathlib,collections;print(collections.Counter(json.load(open(p)).get('status') for p in glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json'))))\"",
"observed": "Binary, verbatim: `var je=[\"busy\",\"shell\",\"idle\",\"waiting\"];function Ke(e){return je.includes(e)?e:void 0}` - the reader's closed set. The producer, verbatim: `function XBe(T){let I=o_o(T);if(I!==void 0)return{status:\"waiting\",waitingFor:I,working:!1};return{status:T.isLoading||T.delegatedActive?\"busy\":\"idle\",waitingFor:void 0,working:T.isQueryActive}}function o_o(T){if(T.queuedElicitation)return\"input needed\";if(T.topDialogWaitingFor!==void 0)return T.topDialogWaitingFor;if(T.pendingWorkerRequest)return\"worker request\";if(T.pendingSandboxRequest)return\"sandbox request\";if(T.isShowingLocalJSXCommand&&!T.isResponseStreaming&&!T.delegatedActive)return\"dialog open\";return}`. Across all six binary generations present on this machine (2.1.229, 2.1.234, 2.1.241, 2.1.251, 2.1.257, 2.1.258) the array is byte-identical: `[\"busy\",\"shell\",\"idle\",\"waiting\"]` - 6/6. The only arrays containing `\"blocked\"` in any of those binaries are `[\"active\",\"idle\",\"blocked\"]` (the `tempo` field), `[\"allow\",\"ask\",\"blocked\"]` / `[\"allow\",\"ask\",\"ask-session\",\"blocked\"]` (permission), `[\"proceed\",\"confirm\",\"blocked\"]` and a task-list vocabulary - never the registry status set. On-disk census over three sampling instants: {idle: 4, busy: 2, shell: 1} then {idle: 6, busy: 2, shell: 1, ...} - `waiting` observed 0 times (no dialog was blocking any session while sampling).",
"rule": "One status observation per registry `.json` file per sampling instant; a value seen zero times on disk is not evidence it cannot occur, which is why the binary's validator array is the authority for the SET and the disk census only for what occurred. Binary side: one distinct array per generation, deduped with `sort -u`.",
"note": "The main body of the claim is confirmed by the binary itself, including the exact dialog sources it lists. Refined on two points: the `blocked` sub-clause is unsupported for every generation testable here, and the row also carries a `waitingFor` companion naming WHICH dialog blocked - a field csift does not read and a stronger HITL signal than `status:waiting` alone. csift code sites confirmed verbatim: src/live/registry.rs 5-9 and src/live/verdict.rs 291-298 unchanged."
}
]
},
{
"id": "REG-003",
"area": "live-registry",
"behavior": "The registry status `shell` is not a working state: the harness computes `idle` and then relabels it `shell` while a local background shell task is open, so `shell` means idle at end of turn WITH a background shell still running. `busy` is the only registry status that signals the session is doing work.",
"depends": "csift's running-shape test accepts only an unreturned tail call or registry `busy`, and folds `shell` into the end-of-turn shape instead, where it also serves as a cross-check on the background section (a `shell` row with nothing counted means a background shell was launched in an unscanned lane or excluded by the lens). Reading `shell` as running - as csift did through v0.10.0 - makes an idle session with a dev server open never satisfy `wait --until stop`.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "303-308",
"snippet": " // The registry's `shell` status is IDLE WITH A BACKGROUND SHELL RUNNING (the binary\n // computes `idle` and then relabels it `shell` while a local_bash task is open) -\n // never a running shape. `busy` is the only registry running signal.\n let registry_status = registry.and_then(|r| r.status.as_deref());\n let registry_shell = registry_status == Some(\"shell\");\n let running_shape = main_tail.unreturned_use.is_some() || registry_status == Some(\"busy\");"
},
{
"path": "src/live/verdict.rs",
"lines": "309-314",
"snippet": " let eot_shape = main_tail.unreturned_use.is_none()\n && (main_tail\n .last_stop_reason\n .as_deref()\n .is_some_and(|s| s == \"end_turn\")\n || registry_shell);"
}
],
"instrument": "Launch one long `run_in_background` shell command in a session, let the turn finish, then read that session's `~/.claude/sessions/<pid>.json`: `status` is `shell` while the command runs and returns to `idle` once it completes, with no prompt in flight either time. Counting rule: one status reading per known background-shell state (open, then closed), taken from a second session so the observed one stays idle.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.1",
"source": "src/live/verdict.rs rank_verdict comment; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'let PIr=W\\(Y_o\\),Nne=VA===\"idle\"&&PIr\\?\"shell\":VA.{0,260}' AND rg -o 'function Y_o\\(FIr\\)\\{return Fhn\\(FIr.tasks\\)\\}' AND rg -o 'function Fhn\\(h\\)\\{.{0,90}\\}function l9t\\(h\\)\\{.{0,70}\\}' AND a 250 ms poll of ~/.claude/sessions/*.json over 240 s recording every (status, statusUpdatedAt) change AND csift status @<session> --format json | jq '.background.tasks[].kind'",
"observed": "Binary, verbatim: `let PIr=W(Y_o),Nne=VA===\"idle\"&&PIr?\"shell\":VA` - the computed status VA is relabelled `shell` only when it is `idle` and PIr is true - and the very next effect writes exactly that relabelled value: `B_o=()=>{l$e({status:Nne,waitingFor:oUe},tUe).catch(J_o)}`. PIr's predicate, verbatim: `function Y_o(FIr){return Fhn(FIr.tasks)}` with `function Fhn(h){return Object.values(h).some(l9t)}function l9t(h){return h.type===\"local_bash\"&&!Fs(h.status)}` - i.e. some unsettled local_bash (background shell) task. Live: in a 240 s poll I caught one session go `shell -> busy` and then `busy -> shell`, the second transition landing 14 666 ms after that turn's newest user-turn record - an end-of-turn transition into `shell`, never into a running state. That same session's background section, read by csift's whole-transcript scan, showed 52 open background tasks including a `shell` kind while its registry row read `shell`.",
"rule": "One quoted producer expression from the binary (the relabel and the write are adjacent statements in one function, so the chain is direct, not inferred). Live side: one status reading per registry file per 250 ms tick over 240 s; a transition counts when the parsed `status` differs from the previous tick's for the same file. Background side: one `kind` per open task row in csift's JSON, counted once.",
"note": "Confirmed at the producer, not just by correlation: `idle` is computed first and only then relabelled, so `shell` is idle-at-end-of-turn with a background shell open. One caveat a reader should know: the harness's own fleet UI renders `busy` and `shell` with the same working glyph (`if(m===\"busy\"||m===\"shell\")return{word:co.working,...}`), so a screenshot of Claude Code's UI is NOT evidence about the registry's meaning - the written state is what counts. Note also that the csift binary on PATH here is 0.10.0, which predates the shell-to-end-of-turn fold; the fold is present in the working tree, and src/live/verdict.rs lines 303-308 and 309-314 match the claim's snippets verbatim."
}
]
},
{
"id": "REG-004",
"area": "live-registry",
"behavior": "`statusUpdatedAt` alone is TRANSITION-written, never a heartbeat: the writer stamps it only when the patch carries a `status`, so an hours-old `statusUpdatedAt` means the state has not changed rather than that the session is gone - a specimen sat `busy` for 2 h 07 m with its pid alive and its transcript written 8 minutes earlier. `updatedAt` is NOT co-equal: sibling writers stamp it on non-status patches (2 of 10 rows had it ahead of `statusUpdatedAt`, by 91 ms and by 93 s), and the file's mtime is not an instrument at all - writers that carry neither timestamp rewrite the row silently (4 of 7 rows had mtime ahead of both, by up to 25.0 h, and three rewrites with a 0 ms `statusUpdatedAt` delta were caught live).",
"depends": "csift renders the `statusUpdatedAt` age as evidence but never derives liveness from registry recency, joining the transcript tail and a pid probe instead; if the harness ever switched to heartbeat writes, the age column would change meaning and the stale-dead branch would need re-deriving.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "30-31",
"snippet": " /// Millisecond epoch of the last status TRANSITION (not a heartbeat).\n pub(crate) status_updated_at_ms: Option<i64>,"
},
{
"path": "src/live.rs",
"lines": "9-12",
"snippet": "//! transitions land sub-second, but the file is written ONLY on transitions (never a\n//! heartbeat: an hours-old `statusUpdatedAt` just means the state has not changed;\n//! the real failure mode is a SIGKILLed session's stale entry, guarded by pid\n//! liveness + a process-start-time check against pid reuse);"
},
{
"path": "src/live/verdict.rs",
"lines": "171-173",
"snippet": " age_secs: r\n .status_updated_at_ms\n .map(|ms| (jiff::Timestamp::now().as_millisecond() - ms).max(0) / 1000),"
}
],
"instrument": "For each `~/.claude/sessions/*.json`, compare `updatedAt` against `statusUpdatedAt` and watch `statusUpdatedAt` across a poll. Do NOT use the file mtime: it advances on writes that touch neither timestamp, so an mtime/timestamp gap is expected, not a refutation.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "AGENTS.md section 1; src/live/registry.rs module doc; src/live.rs module doc; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import json,glob,os,pathlib;[print(json.load(open(p)).get('updatedAt')-json.load(open(p)).get('statusUpdatedAt'), int(os.stat(p).st_mtime*1000)-max(json.load(open(p)).get('updatedAt'),json.load(open(p)).get('statusUpdatedAt'))) for p in glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json'))]\" AND a 200 ms poll of the same directory over 150 s recording every content change AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function l\\$e\\(e,n\\).{0,230}' AND rg -o 'async function XEt\\(e,n\\)\\{await Sn\\(\\{bridgeSessionId:e\\},n\\)\\}'",
"observed": "First instant, 7 rows: `updatedAt - statusUpdatedAt` = 0 ms on 7/7, but `mtime - updatedAt` = 4, 3, 9, 14 ms on four rows and 253 811 ms, 5 676 415 ms and 90 149 163 ms (25.0 h) on the other three - so 0/7 rows had all three equal. Second instant, 10 rows: 2/10 had `updatedAt` AHEAD of `statusUpdatedAt` by 91 ms and 93 131 ms. During a 150 s poll, three separate files were rewritten with `statusUpdatedAt` unchanged (delta exactly 0 ms) and mtime landing 992-1 497 ms later. The writer explains all of it, verbatim: `async function l$e(e,n){let r=Date.now(),...,f=await Sn({...e,updatedAt:r,...e.status!==void 0&&{statusUpdatedAt:r},...},n);...}` - `statusUpdatedAt` is stamped ONLY when the patch carries a status - while sibling writers touch the file without it, e.g. `async function XEt(e,n){await Sn({bridgeSessionId:e},n)}` (neither timestamp) and `async function UHr(e,n){await Sn({messagingSocketPath:e,updatedAt:Date.now()},n)}` (updatedAt only). Substance re-measured on a fresh specimen: one row sat `busy` for 7 635 s (2 h 07 m) with its pid alive under `ps -p` and its transcript last written 468 s ago.",
"rule": "One comparison per registry file per sampling instant: `updatedAt - statusUpdatedAt` and `int(os.stat(p).st_mtime*1000) - max(updatedAt, statusUpdatedAt)`, both in milliseconds. Poll side: a change counts when the (status, statusUpdatedAt, updatedAt, mtime) tuple for one file differs from the previous 200 ms tick.",
"note": "The claim's own stated refutation trigger fired - rows whose mtime advances past `statusUpdatedAt` - but it fired on the INSTRUMENT, not on Claude Code: the behavior is unchanged and is now confirmed at the writer rather than by sample agreement. The `6 of 6 rows agree to the millisecond` figure was an artifact of a small, freshly-transitioned sample; it does not reproduce (0 of 7 here). Anything downstream that treated mtime as a transition marker should stop. csift is unaffected - it reads `statusUpdatedAt` only - and the cited sites src/live/registry.rs 30-31, src/live.rs 9-12 and src/live/verdict.rs 171-173 match verbatim."
}
]
},
{
"id": "REG-005",
"area": "live-registry",
"behavior": "Registry status transitions land sub-second: a session's own row read `status: \"busy\"` with `statusUpdatedAt` equal to that turn's prompt-submit instant.",
"depends": "csift trusts a fresh `busy` as running-shaped evidence with no debounce; a lagging write would make `status` report idle-at-end-of-turn during the first moments of a turn.",
"code": [
{
"path": "src/live.rs",
"lines": "8-9",
"snippet": "//! 1. the harness's session registry (`<claude-home>/sessions/<pid>.json`) - `status`\n//! transitions land sub-second, but the file is written ONLY on transitions (never a"
},
{
"path": "src/live/registry.rs",
"lines": "71",
"snippet": " status_updated_at_ms: v.get(\"statusUpdatedAt\").and_then(serde_json::Value::as_i64),"
}
],
"instrument": "Submit a prompt in one session, then immediately read that session's `~/.claude/sessions/<pid>.json` from a second session: `status` is `busy` and `statusUpdatedAt` is within a second of the prompt instant. Counting rule: one paired reading (submit instant against `statusUpdatedAt`); it needs two concurrent sessions on one machine, since the observing read must not itself be the turn.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "src/live.rs module doc; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "A 200 ms poll of ~/.claude/sessions/*.json over 240 s; on any transition into busy/waiting/shell, read that row's `statusUpdatedAt` and compare it against the newest user-turn record timestamp in the same session's transcript via `csift show @<session> --turn -1 --format json` (field `ts_utc`)",
"observed": "One `shell -> busy` transition caught: `statusUpdatedAt` landed 8 ms BEFORE that turn's newest user-turn record timestamp (signed delta -8 ms), and my poll detected the written file 227 ms after `statusUpdatedAt` with a 200 ms tick - so the file was on disk within roughly one tick of the write. Separately, four brand-new registry rows appeared during a 150 s poll with a detect-lag of 151, 583, 612 and 756 ms between their `statusUpdatedAt` and my observation, all under one second. The mechanism agrees: the write is an effect keyed on the status value and `l$e` stamps `statusUpdatedAt: Date.now()` at write time, so the stamp is the write instant, not a replayed event time.",
"rule": "One paired reading per caught transition: (registry `statusUpdatedAt`) minus (the newest `ts_utc` among the records of that session's last turn), in milliseconds; plus (observation instant) minus `statusUpdatedAt` as an upper bound on write-to-visible latency, which cannot be smaller than the poll interval. Two concurrent sessions are required, since the observing read must not itself be the turn - satisfied here, the observed session was not the one running the probe.",
"note": "Measured, not inferred: 8 ms between the prompt-submit instant and the registry `busy` stamp, on a session other than the observer. The sign is negative (the registry stamp very slightly precedes the transcript record's timestamp), which is consistent with the transcript record being an async flush of the completed message while the registry write is a synchronous effect on the state change. csift code sites src/live.rs 8-9 and src/live/registry.rs line 71 match verbatim."
}
]
},
{
"id": "REG-006",
"area": "live-registry",
"behavior": "The session registry covers TOP-LEVEL sessions only: a subagent transcript has no `<pid>.json` row anywhere, and no per-subagent registry exists in the harness.",
"depends": "csift looks the row up by the OWNER (top-level) session id derived from the transcript path, and when a subagent target finds no row it emits an explicit note and falls back to tail plus children evidence rather than fabricating a row; session lineage is never read out of the registry.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "10-11",
"snippet": "//! TRANSITION-writes only - never a heartbeat. Coverage: TOP-LEVEL sessions only; a\n//! subagent has no row anywhere and csift never fabricates one."
},
{
"path": "src/live/status.rs",
"lines": "63-66",
"snippet": " let owner_id =\n crate::subagent::parent_session_id_from_path(main).unwrap_or_else(|| session_id.clone());\n\n let registry = registry_row_for(&owner_id)?;"
},
{
"path": "src/live/verdict.rs",
"lines": "175-180",
"snippet": " } else if is_subagent_target {\n notes.push(\n \"the registry covers top-level interactive sessions only - a subagent has no \\\n row; verdict from tail + children evidence\"\n .to_string(),\n );"
}
],
"instrument": "`jq -r '.sessionId' ~/.claude/sessions/*.json | sort -u` yields only uuid-shaped ids, never a bare subagent hex; then run `csift status` against a subagent target and read the notes - expect the explicit no-registry note, never a fabricated status. Counting rule: distinct `sessionId` values across the directory, one row per live top-level process; zero rows for subagent ids.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "SPEC.md section 6.13; AGENTS.md section 1; src/live/registry.rs module doc; dev session 2026-08-30; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 counting uuid-shaped vs bare-hex `sessionId` values across ~/.claude/sessions/*.json, intersected against the set of subagent ids taken from every `.../subagents/**/agent-*.jsonl` stem on disk AND csift status @<subagent> --format json for two subagent targets - one whose owner session IS in the registry, one whose owner is NOT",
"observed": "8 registry rows at that instant, `sessionId` matched the 36-char uuid shape on 8/8, bare-hex on 0/8, 8 distinct values (7/7 and 10/10 uuid-shaped at two other instants). 7 536 subagent transcripts on disk; the intersection of their canonical agent ids with the registry's `sessionId` set was 0. csift on a subagent whose owner is unregistered returned verdict `unknown`, zero registry evidence rows, and exactly the note `the registry covers top-level interactive sessions only - a subagent has no row; verdict from tail + children evidence`. csift on a subagent whose owner IS registered attached the OWNER's row as evidence (`surface: registry`, `status busy (pid ...)`), never a fabricated subagent row.",
"rule": "Distinct `sessionId` values across the directory, one row per live top-level process; a subagent id counts as covered only if it appears verbatim as some row's `sessionId`. Subagent ids are the canonical `agent-` stripped stems, one per transcript file.",
"note": "Both halves confirmed with an instrument: zero of 7 536 subagent ids has a row, and csift's two fallback paths behave as claimed - owner-row join when the owner is live, explicit no-row note otherwise. csift code sites src/live/registry.rs 10-11, src/live/status.rs 63-66 and src/live/verdict.rs 175-180 match verbatim."
}
]
},
{
"id": "REG-007",
"area": "live-registry",
"behavior": "Claude Code's task tools persist the session task list as one JSON file per task under `<claude-home>/tasks/<owner>/`, and the owner directory takes either of TWO name forms on real disks: the full session uuid, or the newer `session-` prefix followed by the first 8 characters of that uuid.",
"depends": "csift's tasks section probes BOTH directory names for one owner and merges them; reading only the uuid form silently reports no tasks for every session written under the newer layout, and an absent directory means the session never used the task tools (no section, not an error).",
"code": [
{
"path": "src/live/tasks.rs",
"lines": "1-5",
"snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a\n//! per-session directory. Two directory-name forms exist on real disks (both verified):\n//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file"
},
{
"path": "src/live/tasks.rs",
"lines": "37-41",
"snippet": " let tasks_root = home.join(\"tasks\");\n let mut dirs = vec![tasks_root.join(owner_id)];\n if let Some(prefix) = owner_id.get(..8) {\n dirs.push(tasks_root.join(format!(\"session-{prefix}\")));\n }"
}
],
"instrument": "`ls ~/.claude/tasks/ | sed 's/^session-.*/session-form/;s/^[0-9a-f-]\\{36\\}$/uuid-form/' | sort | uniq -c` - both shapes appear; then `csift status @<session> --format json | jq '{tasks, tasks_completed}'` for a session under each shape (a null `tasks` means no directory, `[]` means a directory with nothing open). Counting rule: one observation per owner directory, classified by name shape. The e2e tests `p13_settled_children_fold_and_tasks_section` and `p14_no_tasks_dir_means_null_not_empty` in tests/cli/live/status.rs pin both forms and the absent-directory case.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6.13; SPEC.md section 6 v0.9.4 ledger; AGENTS.md section 1; CHANGELOG 0.9.4; src/live/tasks.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "ls ~/.claude/tasks/ | sed 's/^session-.*/session-form/;s/^[0-9a-f-]\\{36\\}$/uuid-form/' | sort | uniq -c AND for one owner present in BOTH forms, count `*.json` in each directory and compare against `csift status @<session> --format json | jq '{n_open: (.tasks|length), tasks_completed}'` AND the same csift call for a session with no tasks directory at all",
"observed": "168 owner directories: 152 `session-<8 hex>` form, 16 full-uuid form, 0 of any other shape. Merge check on one owner present in both forms: its uuid-named directory held 7 task files (1 not-completed, 6 completed) and its `session-<first 8>` sibling held 21 (9 not-completed, 12 completed); csift reported exactly 10 open and 18 completed - the union of both directories, with no cap and nothing double-counted. A second owner, present only in the `session-` form with 27 files, reported 10 open / 17 completed = 27. A session with neither directory reported `tasks: null` (not `[]`) and `tasks_completed: null`.",
"rule": "One observation per owner directory, classified by name shape; for the merge, one file per `*.json` in each of the two candidate directories, with `open` counted as status != completed, and the csift numbers must equal the sum over both directories.",
"note": "Both directory forms exist on a live disk in quantity, and the merge is not just claimed but arithmetically checked (7 + 21 files -> 10 open / 18 completed). The three-way distinction the claim draws - null for no directory, [] for an empty one, rows otherwise - reproduced. csift code sites src/live/tasks.rs 1-5 and 37-41 match verbatim."
}
]
},
{
"id": "REG-008",
"area": "live-registry",
"behavior": "Each task file carries `{id, subject, description, status, blocks, blockedBy}` on every file, plus `activeForm` on most (316 of 402 here) and occasionally `owner` or `metadata`; `blocks` and `blockedBy` are arrays. The ids are STRINGS on disk - a short all-digit ordinal of 1 to 3 characters, e.g. `\"13\"` - and the file name is that id. The `status` set is OPEN: `pending`, `in_progress` and `completed` are the values observed on disk, and the harness's own task-update schema additionally accepts `deleted`.",
"depends": "csift treats anything that is not `completed` as an open row rendered with its verbatim status (in-progress rows first, then numeric id order, blockers named), and tolerates a bare-number id; a closed status enum or a numeric-id assumption would drop rows the harness still considers open.",
"code": [
{
"path": "src/live/tasks.rs",
"lines": "5-10",
"snippet": "//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file\n//! carries `{id, subject, description, activeForm, status, blocks, blockedBy}` with\n//! string ids. The set of `status` values is OPEN (pending / in_progress / completed\n//! observed); anything that is not `completed` renders as an open row with its verbatim\n//! status. This is a live-truth read (current values only, no history) - the same\n//! carve-out `status` itself lives under."
},
{
"path": "src/live/tasks.rs",
"lines": "63-66",
"snippet": " if status == \"completed\" {\n report.completed += 1;\n continue;\n }"
},
{
"path": "src/live/tasks.rs",
"lines": "91-97",
"snippet": "/// Ids are strings on disk (\"13\") but tolerate a bare number.\nfn json_id(v: Option<&serde_json::Value>) -> String {\n match v {\n Some(serde_json::Value::String(s)) => s.clone(),\n Some(serde_json::Value::Number(n)) => n.to_string(),\n _ => \"?\".to_string(),\n }"
}
],
"instrument": "`python3 -c \"import json,glob,collections,pathlib;print(collections.Counter(json.load(open(p)).get('status') for p in glob.glob(str(pathlib.Path.home()/'.claude/tasks/*/*.json'))))\"` for the status census and `jq 'keys, (.id|type)' ~/.claude/tasks/*/*.json | sort -u` for the shape. Counting rule: one observation per task `.json` file across both owner-directory forms.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6.13; SPEC.md section 6 v0.9.4 ledger; AGENTS.md section 1; CHANGELOG 0.9.4; src/live/tasks.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import json,glob,pathlib,collections;ps=glob.glob(str(pathlib.Path.home()/'.claude/tasks/*/*.json'));print(len(ps));print(collections.Counter(json.load(open(p)).get('status') for p in ps));print(collections.Counter(k for p in ps for k in json.load(open(p))))\" AND a per-file id type/shape census AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\\[\"pending\",\"in_progress\",\"completed\",\"deleted\"\\]' with surrounding schema",
"observed": "402 task files across the 168 owner directories, 0 unparseable. Status census: completed 270, pending 99, in_progress 33 - exactly the three claimed values, nothing else. Ids: 402/402 JSON strings, 402/402 all-digits, lengths 1 (112), 2 (282), 3 (8); the file-name stem equalled the `id` on 402/402. Key frequency: id 402, subject 402, description 402, status 402, blocks 402, blockedBy 402, but activeForm only 316/402 (78.6%), plus `owner` on 36 and `metadata` on 26. `blocks`/`blockedBy` were JSON arrays on 402/402. The binary's task-update input schema, verbatim, carries a FOURTH value: `qmo=m(()=>c({taskId:i(),status:ee([\"pending\",\"in_progress\",\"completed\",\"deleted\"]).optional(),subject:i().optional(),activeForm:i().optional()}))`, and the create schema makes activeForm optional too: `zmo=m(()=>c({subject:i(),activeForm:i().optional()}))`.",
"rule": "One observation per task `.json` file across both owner-directory forms (402 files); a key counts as present if it appears at the JSON top level, so a key's count below 402 means it is optional. Status census counts one value per file. Binary side: the quoted schema is the tool's own input validator, so it bounds what the harness will ACCEPT, which is wider than what a disk snapshot shows.",
"note": "Ids, the status trio and the arrays all reproduce exactly. Refined on three points the census forced: `activeForm` is present on 78.6% of files rather than all, two further keys occur (`owner`, `metadata`), and the harness accepts a fourth status `deleted` that no file on this disk carries - which is the one value that would render wrongly under the not-completed-means-open rule. Whether a `deleted` task's file survives on disk or is unlinked could not be determined here, since none exists. csift code sites src/live/tasks.rs 5-10, 63-66 and 91-97 match verbatim."
}
]
},
{
"id": "REG-009",
"area": "live-registry",
"behavior": "Only a BACKGROUND session writes a live in-flight registry: at `<claude-home>/jobs/<short-id>/state.json` it records `inFlight: {tasks, queued, kinds[], drainableMonitors, wake?}`. An interactive session returns early and writes nothing there, so no on-disk file states how many background tasks an interactive session currently has armed.",
"depends": "csift's live layer does not read this file, so for an INTERACTIVE session the whole-file transcript scan is the only background-task instrument; if a live-armed count is ever wanted for a background session, this is the only place one exists.",
"code": [
{
"path": "src/live/background.rs",
"lines": "37-39",
"snippet": "//! Never-returned launches sit 56-375 MB before EOF on real files, so this is a whole-\n//! file scan behind a five-needle byte prefilter (measured +0.2-0.4 s worst case), not a\n//! tail read."
}
],
"instrument": "`cat ~/.claude/jobs/*/state.json` on a machine that has only run interactive sessions: either no directory at all, or a row whose `inFlight` counts are zero with an empty `kinds` array. Counting rule: one file per background-job directory; the `kinds` array is populated only for a background session, so absence on an interactive-only machine is the expected observation, not a refutation.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "ls -la ~/.claude/jobs/ and python3 reading every ~/.claude/jobs/*/state.json AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function bre\\(e,n\\)\\{let r=a.CLAUDE_JOB_DIR;.{0,520}' AND rg -o 'inFlight:c\\(\\{tasks:A\\(\\),queued:A\\(\\),kinds:R\\(i\\(\\)\\).{0,190}'",
"observed": "One job directory on this interactive-only machine (an 8-hex short id) plus a top-level `pins.json`; its `state.json` carries `inFlight: {\"tasks\": 0, \"queued\": 0, \"kinds\": [], \"drainableMonitors\": 0}` with `template: bg`, `state: done`, `tempo: idle`, and siblings `timeline.jsonl` and `tmp/`. The writer, verbatim: `async function bre(e,n){let r=a.CLAUDE_JOB_DIR;if(!r||a.CLAUDE_CODE_SESSION_KIND!==\"bg\")return;await p8e(async()=>{...await As(r,{...o,inFlight:{tasks:e.count,queued:d.queued,kinds:[...e.kinds],...e.drainableMonitors!==void 0&&e.drainableMonitors>0&&{drainableMonitors:e.drainableMonitors},...d.wake!==void 0&&{wake:d.wake}},updatedAt:new Date().toISOString()},n)...})}` - the guard returns before any write unless the process has a job directory AND its session kind is `bg`. The declared shape matches: `inFlight:c({tasks:A(),queued:A(),kinds:R(i()),drainableMonitors:A().int().nonnegative().optional(),wake:c({at:A().optional(),reason:i().optional(),fires:A().int().nonnegative(),keepalive:x(!0).optional()}).optional()})`. For contrast, the same machine carried 7-10 session-registry rows and 168 task directories at the same instant.",
"rule": "One file per background-job directory. The `kinds` array is populated only for a background session, so all-zero counts and an empty `kinds` on an interactive-only machine is the expected observation, not a refutation. The early-return guard is read off the writer, so the absence of an interactive in-flight file is established at the producer rather than by absence of evidence.",
"note": "Both halves confirmed. The interactive negative is the interesting one and it is now positive evidence rather than an absence argument: the writer's first statement bails unless `CLAUDE_JOB_DIR` is set and `CLAUDE_CODE_SESSION_KIND` is `bg`, so for an interactive session no on-disk file states how many background tasks are armed and csift's whole-transcript scan remains the only instrument. csift code site src/live/background.rs 37-39 matches verbatim."
}
]
},
{
"id": "REG-010",
"area": "live-registry",
"behavior": "On unix hosts the registry's `procStart` is the owner process's creation instant rendered as an asctime-like string in UTC (`Sun Aug 16 09:04:23 2026`), because the harness literally runs `ps -o lstart=` with `LC_ALL=C` and `TZ=UTC`; a bare `ps lstart` renders the SAME instant in the machine's local zone and locale - measured as a constant 10-hour gap with a 0-second residual on all 7 rows read in a UTC+10 zone. The Windows FILETIME (100-nanosecond ticks since 1601-01-01) is carried in a SEPARATE string key `procStartFt` beside `procStart`, not in `procStart` itself: the row parser accepts both keys, the platform helper writes exactly one of them, and consumers read `procStartFt ?? procStart`.",
"depends": "csift parses the registry value as UTC and the `ps` value as local, then compares INSTANTS with a 2-second tolerance for its pid-reuse guard; comparing the two as strings or as same-zone values flags pid reuse on every row and turns every live session into a stale-dead verdict. When either side is absent or unparseable csift degrades to a pid-only probe and discloses that the reuse guard was skipped.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "13-21",
"snippet": "//! `procStart` is the OWNER PROCESS's creation instant in a PLATFORM-SPECIFIC rendering:\n//! on unix an asctime string in UTC (`Sun Aug 16 09:04:23 2026`), on Windows a FILETIME\n//! integer (100ns ticks since 1601-01-01, e.g. `134328101803820142`). `pidDomain` names\n//! the pid space the row was written in (`darwin`, `linux`, or `win32:<hostname>`); a row\n//! from another domain cannot be probed here and the verdict says so. `ps lstart` renders\n//! in the LOCAL zone - a naive string/local comparison flags pid reuse on EVERY row.\n//! Parse both sides to instants and compare with a small tolerance; when either side is\n//! absent or unparseable, degrade to a pid-only probe AND say so in the evidence (the\n//! reuse guard was skipped, honest, never silent)."
},
{
"path": "src/live/registry.rs",
"lines": "175-188",
"snippet": "/// Parse the registry's `procStart`: an asctime-like UTC string (`Sun Aug 16 09:04:23\n/// 2026`, unix) or a FILETIME integer (`134328101803820142`, Windows: 100ns ticks since\n/// 1601). `None` on any mismatch - the caller degrades to pid-only + a note.\npub(crate) fn parse_registry_proc_start(s: &str) -> Option<jiff::Timestamp> {\n let s = s.trim();\n if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {\n return filetime_to_timestamp(s.parse::<u64>().ok()?);\n }\n let bd = jiff::fmt::strtime::parse(\"%a %b %e %H:%M:%S %Y\", s).ok()?;\n let dt = bd.to_datetime().ok()?;\n dt.to_zoned(jiff::tz::TimeZone::UTC)\n .ok()\n .map(|z| z.timestamp())\n}"
},
{
"path": "src/live/registry.rs",
"lines": "143-152",
"snippet": " PsProbe::Alive(actual) => match (proc_start.and_then(parse_registry_proc_start), actual) {\n (Some(reg), Some(act)) => {\n if (reg.as_second() - act.as_second()).abs() <= 2 {\n PidLiveness::Alive {\n reuse_guard: ReuseGuard::Checked,\n }\n } else {\n PidLiveness::Reused\n }\n }"
}
],
"instrument": "For one live session read `procStart` from its `~/.claude/sessions/<pid>.json` and run `ps -p <pid> -o lstart=`: the two strings differ by exactly the machine's UTC offset while naming one instant. Counting rule: one paired reading per live pid, and seconds between the two parsed instants after zone assignment (expect 2 or fewer); agreement in a UTC+0 zone is evidence of nothing, so run it where the offset is non-zero. The unit test `registry_proc_start_parses_as_utc` in src/live/tests/surfaces.rs pins the parse side.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "SPEC.md section 6.13; AGENTS.md section 1; CHANGELOG 0.9.0; src/live/registry.rs module doc; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "For every row: read `procStart` from ~/.claude/sessions/<pid>.json and run `ps -p <pid> -o lstart=`, then compare the two as instants (registry side assigned UTC, ps side assigned the local zone) AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Eie\\(e\\)\\{try\\{let t=DQ\\(`LC_ALL=C TZ=UTC ps -o lstart= -p \\$\\{e\\}`.{0,60}' AND rg -o 'procStart:typeof o.procStart===\"string\".{0,120}' AND rg -o 'function OU\\(e\\)\\{return c\\(\\)\\?\\{procStart:void 0,procStartFt:e\\}:\\{procStart:e,procStartFt:void 0\\}\\}' AND csift status @<session> --format json | jq '.evidence[] | select(.surface==\"pid\")'",
"observed": "7 of 7 live rows: the registry string and `ps lstart` name the same instant with exactly the machine's UTC+10 offset between them and a 0-second residual after zone assignment (e.g. registry `Tue Sep 1 23:54:26 2026` against ps `Wed 2 Sep 09:54:26 2026`); all 7 pids were alive. The producer explains the UTC rendering exactly: `function Eie(e){try{let t=DQ(`LC_ALL=C TZ=UTC ps -o lstart= -p ${e}`,{timeout:1000});return t?t.trim():void 0}catch{return}}` and its async twin runs `ps -o lstart=` with `env:{...,LC_ALL:\"C\",TZ:\"UTC\"}` - the registry value IS `ps lstart` forced to the C locale and UTC. The Windows half is NOT the same field: the row parser reads two keys, verbatim `procStart:typeof o.procStart===\"string\"?o.procStart:void 0,...typeof o.procStartFt===\"string\"&&{procStartFt:o.procStartFt}` (both string-typed), the platform helper writes one or the other, `function OU(e){return c()?{procStart:void 0,procStartFt:e}:{procStart:e,procStartFt:void 0}}`, and every consumer prefers the FILETIME key, e.g. `let l=d.procStartFt??d.procStart`. End to end, csift's two-sided parse works here: its pid evidence read `alive (start-time guard matched)`, i.e. the two instants agreed within its 2-second tolerance.",
"rule": "One paired reading per live pid: parse the registry string as UTC and the `ps` string as local, then take the absolute difference in seconds (expect <= 2). Agreement in a UTC+0 zone would be evidence of nothing; this host is UTC+10, so the 10-hour string gap and the 0-second instant gap are both meaningful.",
"note": "The unix half is confirmed twice: by measurement on 7 live rows and by the producer command itself, which pins WHY the string is UTC. The Windows parenthetical is corrected - the FILETIME lives under its own key rather than in `procStart` - and this is actionable for csift, since a `procStartFt`-only row silently loses the pid-reuse guard. What remains undecidable here: whether a Windows host writes the FILETIME into the registry row's `procStart` (the row writer passes the platform value straight through under that name) or into `procStartFt` (the shape the messaging-key writer uses). One `<pid>.json` written by a session on a Windows host settles it; a darwin binary cannot. csift code sites src/live/registry.rs 13-21, 139-148 and 171-184 match verbatim."
}
]
},
{
"id": "REG-011",
"area": "live-registry",
"behavior": "The `ps -p PID -o lstart=` form used to probe pid liveness is not universally available on unix: busybox `ps` (Alpine and similar) rejects `-p` and `lstart` outright and fails for a LIVE pid exactly as for a dead one, while `/proc/<pid>` answers liveness directly on Linux and does not exist on macOS, where the `ps` form is reliable.",
"depends": "csift falls back to a `/proc/<pid>` directory test when the `ps` form fails, keeps the no-such-process verdict only when that is absent too, and discloses the skipped reuse guard; without the fallback `status` read a live session as stale-dead on musl hosts, which the release matrix's musl lanes caught.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "201-203",
"snippet": "/// One `ps -p PID -o lstart=` call: a failing/empty result = no such process; success\n/// yields the start instant when the LOCAL-rendered format parses (two observed field\n/// orders tried), else `Alive(None)` - the caller then skips the reuse guard AND says so."
},
{
"path": "src/live/registry.rs",
"lines": "213-222",
"snippet": " if !out.status.success() || text.is_empty() {\n // busybox ps (Alpine and friends) rejects `-p`/`lstart` outright, so the probe\n // fails for a LIVE pid too. On Linux `/proc/<pid>` answers liveness directly:\n // present = alive with the start time unknown (the reuse-guard skip is\n // disclosed); absent (or no /proc at all, as on macOS where the ps form is\n // reliable) = the no-such-process verdict stands.\n if std::path::Path::new(&format!(\"/proc/{pid}\")).is_dir() {\n return PsProbe::Alive(None);\n }\n return PsProbe::NoProcess;"
}
],
"instrument": "In a busybox container run `ps -p 1 -o lstart=` (non-zero exit or empty output) beside `test -d /proc/1` (succeeds), then `csift status` for a live session there must not report stale-dead and must name the reuse guard as skipped. Counting rule: one probe outcome per platform family (busybox, glibc Linux, macOS); only a real run on a busybox host exercises the fallback. The unit tests `probe_pid_own_process_guard_states` and `probe_pid_reports_a_reaped_pid_dead` in src/live/tests/surfaces.rs pin the alive and dead arms.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.1",
"source": "SPEC.md section 6 v0.9.1 ledger; CHANGELOG 0.9.1; src/live/registry.rs ps_probe comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "docker run --rm busybox:latest sh -c 'ps -p 1 -o lstart=; echo exit=$?; ps -o lstart=; echo exit=$?; test -d /proc/1 && echo proc1=present; busybox | head -1' || docker run --rm debian:stable-slim sh -c 'apt-get -qq update >/dev/null && apt-get -qq install -y procps >/dev/null; LC_ALL=C ps -p 1 -o lstart=; test -d /proc/1 && echo present; ps --version' || on the macOS host: ps -p $$ -o lstart=; echo exit=$?; ls -d /proc",
"observed": "busybox v1.38.0: `ps: invalid option -- 'p'` with exit=1, and `ps: bad -o argument 'lstart', supported arguments: user,group,comm,args,pid,ppid,pgid,etime,nice,rgroup,ruser,time,tty,vsz,sid,stat,rss` with exit=1 - both for pid 1, which is the container's own live init; `test -d /proc/1` succeeded (proc1=present). glibc Linux (procps-ng 4.0.4): `ps -p 1 -o lstart=` printed `Wed Sep 2 09:47:54 2026` and /proc/1 was present. macOS (Darwin 24.6.0, arm64): `ps -p $$ -o lstart=` printed `Wed 2 Sep 19:46:12 2026` with exit=0, and `ls -d /proc` returned `ls: /proc: No such file or directory`.",
"rule": "One probe outcome per platform family (busybox container, glibc Linux container, macOS host), each family probed once. A container's pid 1 is live by construction, so a failing ps there is a measured false-dead rather than a dead pid; /proc presence is a single directory test.",
"note": "Code site verified verbatim in the current file: src/live/registry.rs lines 197-199 (the ps_probe doc comment) and 209-218 (the failure arm with the /proc fallback), both matching the ledger snippets character for character; the named unit tests probe_pid_own_process_guard_states (src/live/tests/surfaces.rs:44) and probe_pid_reports_a_reaped_pid_dead (src/live/tests/surfaces.rs:312) exist. Scope limit worth stating: only the two primitives the fallback keys on were measured in the busybox container. The end-to-end half - csift status not reporting stale-dead there - was not exercised, because the csift binary on this machine is macOS arm64 and the container carries no ~/.claude corpus; deciding that half needs a musl build run inside a busybox host against a fixture home. Cross-claim aside for REG-010's owner, found while reading the same probe path in the binary: the Windows rendering is a SEPARATE FIELD, not the same field re-encoded - the binary carries `function OU(e){return c()?{procStart:void 0,procStartFt:e}:{procStart:e,procStartFt:void 0}}`, so a Windows row writes `procStartFt` and leaves `procStart` undefined."
}
]
},
{
"id": "REG-012",
"area": "live-registry",
"behavior": "`ps -o lstart=` renders its date in two field orders - `Sun Aug 16 09:04:23 2026` (%a %b %e) and `Sun 16 Aug 09:04:23 2026` (%a %e %b) - but the discriminator is the ps implementation together with LC_TIME, not the host family. A single macOS host emits both: C and en_US give month-first, en_AU and en_GB give day-first. glibc procps-ng emits month-first in every locale tried, so it contributes only one order. Both orders reach csift in production: on a machine whose LC_TIME is day-first the default probe returns the second order and the pid-reuse guard still matches.",
"depends": "csift tries both patterns before giving up and degrading to a disclosed pid-only probe; matching only one order silently disables the pid-reuse guard on the other family of hosts, which shows up as an `alive (pid only)` evidence value rather than as an error.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "224-234",
"snippet": " let local = crate::timez::local_tz();\n for fmt in [\"%a %b %e %H:%M:%S %Y\", \"%a %e %b %H:%M:%S %Y\"] {\n if let Ok(bd) = jiff::fmt::strtime::parse(fmt, &text) {\n if let Ok(dt) = bd.to_datetime() {\n if let Ok(z) = dt.to_zoned(local.clone()) {\n return PsProbe::Alive(Some(z.timestamp()));\n }\n }\n }\n }\n PsProbe::Alive(None)"
}
],
"instrument": "Compare `LC_ALL=C ps -p $$ -o lstart=` against `LC_ALL=en_AU.UTF-8 ps -p $$ -o lstart=` on ONE host (macOS shows both orders, glibc procps-ng shows one), then confirm the guard still binds by reading the `pid` evidence row of `csift status @<session-uuid> --format json` under each locale.",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/live/registry.rs ps_probe comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "locale; ps -p $$ -o lstart=; LC_ALL=C ps -p $$ -o lstart=; LC_ALL=en_US.UTF-8 ps -p $$ -o lstart=; LC_ALL=en_AU.UTF-8 ps -p $$ -o lstart=; LC_ALL=en_GB.UTF-8 ps -p $$ -o lstart= || docker run --rm debian:stable-slim sh -c 'apt-get -qq install -y procps locales; LC_ALL=C ps -p 1 -o lstart=; locale-gen en_AU.UTF-8; LC_ALL=en_AU.UTF-8 ps -p 1 -o lstart=; ps --version' || csift status @<session-uuid> --format json | (select the evidence row whose surface is pid) and LC_ALL=C csift status @<session-uuid> --format json | (same selection)",
"observed": "One macOS host, LC_TIME=en_AU.UTF-8 by default, produced BOTH orders: default and LC_ALL=en_AU.UTF-8 and LC_ALL=en_GB.UTF-8 all printed `Wed 2 Sep 19:46:24 2026` (day before month, day space-padded to two columns), while LC_ALL=C and LC_ALL=en_US.UTF-8 printed `Wed Sep 2 19:46:24 2026` (month before day). The glibc Linux host (procps-ng 4.0.4) printed `Wed Sep 2 09:47:54 2026` under BOTH LC_ALL=C and LC_ALL=en_AU.UTF-8 - one order only. csift's pid evidence row was `{\"age_secs\": null, \"surface\": \"pid\", \"value\": \"alive (start-time guard matched)\"}` in the default locale AND under LC_ALL=C.",
"rule": "One rendering per (ps implementation, LC_TIME) pair, not per host: the same host is counted twice when LC_TIME changes the order. A third unmatched order would not error - it surfaces as the pid evidence value `alive (pid only)` instead of `alive (start-time guard matched)`, so the csift run is the discriminating counter.",
"note": "Code site verified verbatim: src/live/registry.rs lines 220-230, with the two format strings `\"%a %b %e %H:%M:%S %Y\"` and `\"%a %e %b %H:%M:%S %Y\"` on line 221 exactly as the ledger quotes. The day-first form pads the day to two columns, so the literal text carries a DOUBLE space (`Wed 2 Sep`); the %e directive absorbs it, confirmed end to end by the guard matching rather than degrading. The claim's substance - at least two orders exist and both must be tried - stands; only the attribution to host families and the per-host-family counting rule needed correcting, and that matters because it means a single-platform release matrix cannot rule the second order out."
}
]
},
{
"id": "REG-013",
"area": "live-registry",
"behavior": "A registry row outlives the process it describes: SIGKILL writes no closing transition, so the file stays on disk carrying its last `status` and `statusUpdatedAt` while its pid names nothing. It is NOT kept until it is replaced, though - any peer session may run the registry sweep, which deletes every `<pid>.json` whose pid is dead (and reports `Prior session exited uncleanly` for interactive rows). Measured removal latency after a SIGKILL ranged from under 200 ms to still-present 35 minutes later on the same machine, so a stale row is a race won or lost, never a guarantee.",
"depends": "the owner-pid probe is the only thing that retires a stale row: csift ranks a dead or reused pid above every other surface and reports stale-dead, naming whether the tail was mid-tool or settled when the process ended; trusting the row alone would report long-dead sessions as running.",
"code": [
{
"path": "src/live.rs",
"lines": "10-12",
"snippet": "//! heartbeat: an hours-old `statusUpdatedAt` just means the state has not changed;\n//! the real failure mode is a SIGKILLed session's stale entry, guarded by pid\n//! liveness + a process-start-time check against pid reuse);"
},
{
"path": "src/live/registry.rs",
"lines": "124-128",
"snippet": "/// Probe pid liveness WITHOUT signaling (and without a second `unsafe` site - the\n/// crate's single-allow law outranks the raw-syscall form): one process query answers\n/// both questions - a failing/empty query = no such process; a start time within\n/// tolerance of the registry's `procStart` = the same process (reuse guarded). A row\n/// from another pid domain is never probed (its pid belongs to another machine or OS)."
},
{
"path": "src/live/verdict.rs",
"lines": "290",
"snippet": " let dead = matches!(liveness, Some(PidLiveness::Dead | PidLiveness::Reused));"
}
],
"instrument": "Spawn a throwaway session, poll ~/.claude/sessions/*.json at a fixed interval, SIGKILL the registered pid, and keep polling: record both whether the row survives and how long. Confirm death with `ps -p <pid>`; read the sweep contract out of the binary with `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'sessionRegistry\\] sweep.{1500}'`.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "src/live.rs module doc; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "A scratchpad python pty.fork probe spawned a throwaway Claude Code session in a trusted directory with the inherited child-session env markers stripped (CLAUDE_CODE_CHILD_SESSION, CLAUDECODE, CLAUDE_CODE_ENTRYPOINT, CLAUDE_PID, CLAUDE_CODE_SESSION_ID, the messaging socket/token), while a second process polled ~/.claude/sessions/*.json every 150 ms, sent SIGKILL to the registered pid at t+40 s, and kept polling to t+75 s; then `ps -p <pid> -o pid=,comm=` and a re-read of the row minutes later. Binary side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'sessionRegistry\\] sweep.{1500}'",
"observed": "The row appeared 5.23 s after spawn with `\"status\": null`, then transitioned to `\"idle\"` 0.33 s later (t+5.56 s). SIGKILL was sent at t+40.02 s; the 150 ms poller printed no removal through t+75 s, and a re-read ~10 minutes later still found the file with its pre-kill `status: idle` and an unchanged `statusUpdatedAt`, while `ps -p <pid>` exited 1 with empty output. A separate earlier probe's row was already gone at the first poll 0.2 s after its SIGKILL and stayed gone at 1/2/3/5/8/12/20/30/45/60 s. The binary explains both: the sweep logs `[sessionRegistry] sweep ${d?`permitted (domain ${f})`:\"declined by isRegistrySweepPermitted() - dead records are left in place (neither counted nor deleted)\"}`, and its dead-pid arm deletes the `<pid>.json` and reports `Prior session exited uncleanly: <sessionId> (v<version>)` when the swept row's `kind===\"interactive\"`.",
"rule": "One row reading before the kill, then one per 150 ms poll after it, plus one late re-read; `ps -p <pid>` exit status is the liveness oracle (exit 1 with no output = no such process). Four spawn/kill cycles were run; two registered a row, of which one kept the row well past the kill and one lost it inside 200 ms.",
"note": "Code sites verified verbatim in the current files: src/live.rs lines 10-12, src/live/registry.rs lines 120-124, src/live/verdict.rs line 290. Two honest scope limits. First, the stale row I captured carried `idle`, not `busy`; the mechanism is status-agnostic (whatever the last transition wrote is what persists), so the claim's `busy` example is an illustration rather than something measured here. Second, the csift-side half - that `status` then reports stale-dead - could not be exercised: a session killed before its first prompt writes no transcript, and `csift status @<session-uuid>` bails with `no session file found for session id [...]`. Exercising that needs a session that took at least one turn before being killed. The refinement matters operationally: because a peer can sweep the row away within a fraction of a second, csift's stale-dead verdict is reachable only while the row survives, and after a sweep the same dead session degrades to the no-registry lane instead."
}
]
},
{
"id": "REG-014",
"area": "live-registry",
"behavior": "A registry row's `kind` is a closed four-value enum - `interactive`, `bg`, `daemon`, `daemon-worker` - stamped once when the row is registered, from the session's own kind (a `bg` row additionally carries `jobId` and `spare`). Row updates merge a patch into the existing row, and no code path in 2.1.258 rewrites `kind` to a default. What is genuinely not durable is the ROW: the registry sweep deletes any `<pid>.json` whose pid is dead, regardless of kind, which is why a background job's row disappears once the job is killed.",
"depends": "csift parses only the liveness-relevant fields (pid, status, statusUpdatedAt, procStart, pidDomain) and ignores `kind` entirely, so no verdict, lane or lineage is ever derived from it.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "25-27",
"snippet": "/// One registry row (only the liveness-relevant fields; the rest is tolerated + ignored).\n#[derive(Debug, Clone)]\npub(crate) struct RegistryRow {"
},
{
"path": "src/live/registry.rs",
"lines": "71",
"snippet": "status_updated_at_ms: v.get(\"statusUpdatedAt\").and_then(serde_json::Value::as_i64),"
}
],
"instrument": "Read the enum and the writer straight out of the binary (`strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'kind:.{0,60}\"interactive\".{0,120}'`), then watch a live row: poll ~/.claude/sessions/*.json and dump each new row's full JSON, correlating appearances and removals against ~/.claude/daemon.log `[bg]` lines.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "A 100 ms watcher recorded the full JSON of every row appearing under ~/.claude/sessions/*.json across four spawn/SIGKILL cycles of a throwaway session, plus a python census of every row's kind/status/entrypoint and pid liveness; then strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'kind:[A-Za-z_$]+\\(\\)\\.(enum|optional)\\(.{0,200}|kind:.{0,60}\"interactive\".{0,120}' and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{80}o===\"interactive\"\\?\\{name:.{1400}'; and tail of ~/.claude/daemon.log for the background-job window.",
"observed": "The binary carries a closed schema `kind:w.enum([\"interactive\",\"bg\",\"daemon\",\"daemon-worker\"])`. The writer stamps it once at registration: `{pid:process.pid,sessionId:...,cwd:...,startedAt:Date.now(),procStart:...,version:...,peerProtocol:...,peerFeatures:...,kind:o,entrypoint:...,pidDomain:...,...{jobId:o===\"bg\"&&a.CLAUDE_JOB_DIR?...:void 0,spare:d?!0:void 0}}`, and the update path re-reads the file and writes `{...existing,...patch}` (no default reintroduces `kind`). Live: every row whose JSON I actually parsed over ~50 minutes carried `kind: \"interactive\"` - 7 pre-existing rows plus 4 probe rows. Two further rows appeared and vanished inside a background-job window (~/.claude/daemon.log: `[bg] bg spare spawned host pid=<n>`, `[bg] bg claimed-spare <id> (slash)`, then `[bg] bg settled <id> (killed)`), but they were removed before their `kind` was read.",
"rule": "One `kind` reading per registry row per 100 ms poll; a row counts as observed only if its JSON was parsed while the file still existed. Binary strings count as one observation each and are quoted verbatim.",
"note": "Code sites verified verbatim: src/live/registry.rs lines 25-27 and 65-74; the parsed field set is exactly pid, status, statusUpdatedAt, procStart, pidDomain, so `kind` is indeed never read by csift and no verdict, lane or lineage can depend on it. The claim's second half (the row vanishing when the job is killed) is explained and consistent with what I saw. The first half - a `bg` row reverting to `interactive` - could NOT be reproduced: no `kind:\"bg\"` row could be captured, because background jobs are spawned by the harness daemon rather than on demand from this lane, and the binary shows no mechanism that would rewrite the field. What would decide it: watch a single row for a daemon-spawned background job (identified from the ~/.claude/daemon.log line `[bg] bg spawned <id>`), sampling its `kind` while the job runs and again after `[bg] bg settled <id> (killed)`. Either way csift's posture is unaffected, since it ignores the field."
}
]
},
{
"id": "REG-015",
"area": "live-registry",
"behavior": "Neither harness store carries background-task state: no session registry row mentions a monitor or a task id, and the task directory holds only the session task list, never a 9-character background task id.",
"depends": "csift therefore reconstructs background tasks from the transcript alone (a whole-file scan behind a byte prefilter) and reads the task directory only as the task-list section; a future store that did carry live task state would let that scan be replaced by a cheap read.",
"code": [
{
"path": "src/live/tasks.rs",
"lines": "1-3",
"snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a"
},
{
"path": "src/live/background.rs",
"lines": "1-2",
"snippet": "//! Background tasks: the off-turn work a session launched and whether it ever came\n//! back. Three kinds, measured on the corpus (v0.10.0):"
}
],
"instrument": "`grep -ril 'monitor\\|taskId' ~/.claude/sessions/` and `grep -rlE '\"b[a-z0-9]{8}\"' ~/.claude/tasks/` - both list nothing. Counting rule: one hit equals one file; zero files from either grep is the expected observation, taken while at least one background task is known to be open so absence is not merely an idle machine.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; src/live/tasks.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "grep -ril 'monitor\\|taskId' ~/.claude/sessions/ | wc -l ; grep -rlE '\"b[a-z0-9]{8}\"' ~/.claude/tasks/ | wc -l ; run in the same command as csift status @<session-uuid> --format json (reading the evidence row whose surface is background) to establish the background-open precondition; plus a python key-union census over ~/.claude/sessions/*.json and ~/.claude/tasks/*/*.json",
"observed": "Both greps returned 0 files. The simultaneous csift evidence row was `{\"age_secs\": null, \"surface\": \"background\", \"value\": \"2 open; 165 completed, 1 failed, 15 killed, 0 stopped\"}`, so at least two background tasks were open at that instant. Scope scanned: 16 directory entries under ~/.claude/sessions (8 rows plus 8 key sidecars) and 402 task JSON files. Key union across the registry rows was 19 keys - bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version - none naming a monitor or a task. Key union across all 402 task files was activeForm, blockedBy, blocks, description, id, metadata, owner, status, subject.",
"rule": "One hit equals one file; both greps are recursive over the whole store, so zero files means no file in either store contains the needle. The background-open precondition is met by the csift `background` evidence row read in the same command, which is what makes the zero a real absence rather than an idle machine.",
"note": "Code sites verified verbatim: src/live/tasks.rs lines 1-3 and src/live/background.rs lines 1-2. The key-union census strengthens the greps: neither store has any field that could carry a background task id under a different name, so the whole-file transcript scan really is the only instrument for background tasks. One aside for REG-008's owner, seen in the same census: the 402 task files carry two keys beyond the seven that claim lists - `metadata` and `owner` - and neither is a background task id, so REG-015 is unaffected."
}
]
},
{
"id": "REG-016",
"area": "live-registry",
"behavior": "The session-registry directory holds more than rows: beside every `<claude-home>/sessions/<pid>.json` sits a companion file `<pid>.<64 lowercase hex>.key` sharing the row's pid stem, one per row (7 rows and 7 companions, a 1:1 pid-stem pairing that held across 13414 directory listings over 90 seconds). The companion is NOT unparseable: it is itself a small JSON object (87 or 108 bytes; keys peerToken and procStart, sometimes pidDomain), so a reader that treats every directory entry as a registry row parses it SUCCESSFULLY and must reject it on content - it carries no `sessionId` - rather than on a parse error. The directory can also hold a third entry kind that is neither: Claude Code writes and later unlinks a `.fleetview-heartbeat` dotfile in the same directory whose whole content is a millisecond epoch as a bare integer string (absent at all 13414 listings here, so its lifetime was not observed). csift's `.json` extension filter excludes all of these before any read.",
"depends": "csift's registry lookup filters on the `.json` extension before it reads anything, so the companions never enter the scan; without that filter every live session costs one read plus one failed parse, and because an unparseable entry is skipped silently the waste would never surface as an error. The filter also means any future sidecar the harness writes under a `.json` name would be indistinguishable from a row and would have to be rejected on content instead.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "45-50",
"snippet": " for entry in std::fs::read_dir(&dir)? {\n let entry = entry?;\n let p = entry.path();\n if p.extension().and_then(|e| e.to_str()) != Some(\"json\") {\n continue;\n }"
},
{
"path": "src/live/registry.rs",
"lines": "38-44",
"snippet": "/// Scan the registry dir for the row whose `sessionId` matches. `Ok(None)` when the dir\n/// is absent or no row matches (a subagent target, a non-interactive session, an old CC).\npub(crate) fn registry_row_for(session_id: &str) -> Result<Option<RegistryRow>> {\n let dir = crate::path::claude_home()?.join(\"sessions\");\n if !dir.is_dir() {\n return Ok(None);\n }"
}
],
"instrument": "`ls -a ~/.claude/sessions | sed 's/.*\\.//' | sort | uniq -c` - the `json` and `key` counts are equal, and `ls -a` is needed because the third entry kind is a dotfile. Then match each companion against `<digits>.<64 hex>.key`, check that its pid stem names an existing row, and JSON-parse it (it parses; the discriminator against a row is the absent `sessionId`, not a parse failure). Counting rule: one observation per directory entry, bucketed by extension, per listing - a single listing gives a per-instant pairing, and repeating the listing (13414 times over 90s here) is what turns it into a windowed statement. The pairing is still not a durable invariant: a session starting or exiting mid-listing can be seen half-paired.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.0",
"source": "dev session 2026-09-02; src/live/registry.rs registry_row_for"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "ls -a ~/.claude/sessions | sed 's/.*\\.//' | sort | uniq -c ; then, per companion, check the name against <digits>.<64 lowercase hex>.key and check that its pid stem names an existing <pid>.json ; then python3 -c 'json.loads(open(k).read())' over each .key ; then a 90-second poll at 5ms listing the directory and reading every *.json (script kept in the run scratchpad, not the repo)",
"observed": "7 json + 7 key + 2 blank (the . and .. entries of ls -a), no dotfiles; all 7 companions have a digits-only pid stem, a 64-character all-lowercase-hex middle segment, and an existing <pid>.json sibling; json stems minus key pid stems = 0 and key pid stems minus json stems = 0; all 7 .key files are 87 or 108 bytes and ALL 7 PARSE AS JSON, each a JSON object whose keys are peerToken + procStart, three of them also pidDomain; the 90s poll made 13414 listings, read 93898 *.json files (exactly 7.0 per listing), saw 0 parse failures, 0 empty reads, and 0 directory entries with any extension other than json or key",
"rule": "One observation per directory entry, bucketed by extension, per listing. The single-instant census is 7/7; the pairing is then checked as two set differences over pid stems (both empty = 1:1). The .key content check counts one JSON-parse attempt per companion file. The poll re-runs the extension census 13414 times over 90 seconds, so 'only json and key ever appeared' is a 90-second statement, not a one-instant one.",
"note": "Both code sites confirmed verbatim in the current file: src/live/registry.rs lines 38-44 hold the doc comment through the `if !dir.is_dir()` guard exactly as quoted, and lines 45-50 hold the `read_dir` loop through the `Some(\"json\")` extension filter exactly as quoted; no path or line correction needed. What the instrument refutes is the claim's consequence clause, which was never itself measured: the companion parses fine as JSON, so the cost a naive reader pays is one wasted read plus a content-level rejection per live session, not a parse failure. That is the same waste and the same silence, but the ledger should not assert a parse failure that does not happen. The `.fleetview-heartbeat` dotfile is a second reason the extension filter is load-bearing and was not in the claim at all."
}
]
},
{
"id": "REG-017",
"area": "live-registry",
"behavior": "Claude Code ships an atomic-write helper that writes through a temporary file and a rename, plus a documented non-atomic fallback whose error is prefixed `atomic write failed first: `. Both paths are present in the shipped binary. But the binary DOES attribute one of the four JSON sidecars a reader joins: the session-registry row `<claude-home>/sessions/<pid>.json` is written with plain `fs/promises.writeFile` on both its registration and its update path (and the sibling `.fleetview-heartbeat` likewise), NOT through the atomic helper - the writing module's own import header binds `writeFile as Mg` alongside the `join` and `readFile` aliases that same function uses. A plain writeFile truncates in place, so the registry row is not merely 'to be assumed' observable half-written: it is positively established as such, and csift's skip-on-unparseable guard is required rather than merely prudent. The other three sidecars - subagent `meta.json`, workflow run manifests, harness task files - remain unattributed by this instrument; a serialized write chain (`pidFileWriteChain`) orders the registry row's writers against each other but does nothing for a concurrent external reader.",
"depends": "csift reads all four sidecars as a whole-file read plus a parse and silently skips a file that fails either step, so one caught mid-write yields a DOWNGRADED answer with no disclosure: `status` falls back to its no-registry lane, a task row disappears from the tasks section, and a subagent loses its type and its teammate discrimination. Retrying the read once, or disclosing the skip as evidence, is the only way that failure could become visible.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "51-56",
"snippet": " let Ok(raw) = std::fs::read_to_string(&p) else {\n continue; // a mid-write or unreadable row is not this session's problem\n };\n let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {\n continue;\n };"
},
{
"path": "src/subagent/meta.rs",
"lines": "84-93",
"snippet": "pub(crate) fn read_meta(meta_path: Option<&Path>) -> MetaFields {\n let Some(p) = meta_path else {\n return MetaFields::default();\n };\n let Ok(bytes) = std::fs::read(p) else {\n return MetaFields::default();\n };\n let Ok(v) = serde_json::from_slice::<serde_json::Value>(&bytes) else {\n return MetaFields::default();\n };"
}
],
"instrument": "`strings -n 40` over the installed Claude Code binary grepped for `atomic write failed first` shows the helper (temp file, symlink-refusing O_EXCL open, fsync, rename) and its in-place fallback; `strings -n 12 <binary> | grep -oE 'appendFileSync|renameSync|mkdtempSync' | sort | uniq -c` gives 24 / 15 / 11 and bounds call sites from above without attributing any. To ATTRIBUTE a sidecar, do not count - resolve the alias: find the writer by a literal only it contains (for the registry row, ``Ct(e1(),`${process.pid}.json`)``), then find the nearest preceding `import{...}from\"fs/promises\"` header in the same module text and require that every identifier the writer calls is bound there. The registry row resolves to plain `writeFile`. A live race (poll `~/.claude/sessions/*.json` at high frequency and count reads that fail to parse) can corroborate but must report its denominator: a 90-second 5ms poll here made 13414 listings and 93898 reads yet saw 0 row mtime changes, so its 0 parse failures prove nothing - a race with no write in the window is unqualified.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.0",
"source": "dev session 2026-09-02; src/live/registry.rs registry_row_for; src/subagent/meta.rs read_meta"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 40 ~/.local/share/claude/versions/2.1.258 | rg 'atomic write failed first' ; strings -n 12 ~/.local/share/claude/versions/2.1.258 | grep -oE 'appendFileSync|renameSync|mkdtempSync' | sort | uniq -c ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Sn\\(e,n\\)\\{let r=Ct\\(e1\\(\\),`\\$\\{process\\.pid\\}\\.json`\\).{0,60}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'let f=K\\(await ace\\(r,\"utf8\"\\)\\);return await Mg\\(r,S\\(\\{\\.\\.\\.f,\\.\\.\\.e\\}\\)\\),!0' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'readFile as ace,unlink as Oi,writeFile as Mg\\}from\"fs/promises\"' ; plus the 90-second 5ms poll of ~/.claude/sessions described under REG-016",
"observed": "The atomic helper is present verbatim: a temp name `.tmp.${process.pid}.${q(6).toString(\"hex\")}`, an O_WRONLY|O_CREAT|O_EXCL open with O_NOFOLLOW, an fsync, then `Renaming ${m} to ${o}` and `File ${o} written atomically`; the fallback is present verbatim as `File ${o} written via in-place fallback` guarded by an O_TRUNC reopen, and the documented error decoration is ` (atomic write failed first: ${l(r)})`. Counts reproduce exactly: appendFileSync 24, renameSync 15, mkdtempSync 11. The session-registry writer is `function Sn(e,n){let r=Ct(e1(),`${process.pid}.json`),o=BG(),d=o.pidFileWriteChain.then(async()=>{...})}` and its direct-filesystem branch is `let f=K(await ace(r,\"utf8\"));return await Mg(r,S({...f,...e})),!0`; the registration branch is `else await Mg(v,N)` and the heartbeat is `await Mg(Ct(e1(),wl),String(Date.now()))` with `var wl=\".fleetview-heartbeat\"`. That module's own import header binds all three aliases the writer uses: `readFile as ace,unlink as Oi,writeFile as Mg}from\"fs/promises\"` in the same statement as `join as Ct` from \"path\". So Mg is fs/promises.writeFile - a plain truncating write, NOT the atomic helper. The 90-second poll observed 0 `.tmp.` sightings and 0 parse failures over 93898 reads, but ALSO 0 row mtime changes, so it witnessed no write at all and decided nothing.",
"rule": "For the string counts: one occurrence per matching literal in one binary's string table; a count bounds call sites from above and attributes none of them to a sidecar. For the attribution: an alias is resolved by finding the nearest preceding `import{...}from` header in the same module text and requiring that ALL of the identifiers the target function actually calls (here Ct, ace, Mg) are bound by that one header - a three-way agreement, not a single-name guess. For the poll: writes-observed is the denominator (row mtime changes); with a denominator of 0 the parse-failure and temp-file counts are vacuous and the poll is unqualified.",
"note": "Both code sites confirmed verbatim in the current files: src/live/registry.rs lines 51-56 hold the read-then-parse pair with the `// a mid-write or unreadable row is not this session's problem` comment exactly as quoted, and src/subagent/meta.rs lines 84-93 hold `read_meta` through its `from_slice` guard exactly as quoted; no path or line correction needed, and the `depends` reasoning about the silent downgrade is unaffected. The refinement is to the premise only. I could not resolve the remaining three sidecars: a partial trace found the subagent metadata writer calling a 3-arity helper `Un(path, content, mode)` and separately found a `function Un(t,n,e,r){return QQ(t,n,{mode:e,renameFn:r})}` whose body is a temp-and-rename writer, but those two sit in different string-table regions and the identifier is chunk-locally mangled, so the join does not meet the three-way-agreement bar the registry row met and I am not asserting it. Deciding those three needs either the same alias-join done properly per module, or a race with a non-zero write denominator - which on this machine means catching a session status TRANSITION, not merely a busy session, since 90 seconds of continuous activity produced zero row writes."
}
]
},
{
"id": "TAIL-001",
"area": "live-tail",
"behavior": "A tool call in flight appears on disk as an assistant `tool_use` block whose `id` has no later `tool_result` carrying that `tool_use_id`; a process that died mid-tool leaves exactly the same shape, so the transcript alone cannot separate the two.",
"depends": "csift's `tail_shape` reads a bounded 512 KB final window, walks it BACKWARD collecting `tool_result` ids first, and reports the newest use id absent from that set as the unreturned call; `status`/`wait` classify the tail by SHAPE (unreturned use, last stop_reason, last record instant) and the pid probe is what disambiguates in-flight from dead mid-tool.",
"code": [
{
"path": "src/live/tail.rs",
"lines": "3-7",
"snippet": "//! Reads the FINAL window of a transcript (bounded, never the whole file), walks it\n//! backward, and reports the liveness-relevant shape: the newest UNRETURNED tool call\n//! (a use whose id has no later result = a tool in flight, or a process dead mid-tool),\n//! the last assistant `stop_reason`, and the last record's instant. A record is only\n//! trusted from a COMPLETE line (torn tails are skipped by the newline framing)."
},
{
"path": "src/live/tail.rs",
"lines": "65-67",
"snippet": " // Backward walk: result ids seen so far are LATER in file order, so a use id absent\n // from that set is unreturned as of the tail.\n let mut later_result_ids: std::collections::HashSet<String> = std::collections::HashSet::new();"
},
{
"path": "src/live/tail.rs",
"lines": "93-107",
"snippet": " if shape.unreturned_use.is_none() {\n for b in blocks {\n if let crate::model::Block::ToolUse {\n id: Some(id), name, ..\n } = b\n {\n if !later_result_ids.contains(id) {\n shape.unreturned_use = Some((\n name.clone().unwrap_or_else(|| \"(unnamed)\".to_string()),\n rec.timestamp.clone(),\n ));\n }\n }\n }\n }"
}
],
"instrument": "During a long SYNCHRONOUS Bash call, `csift status @<session>` names the unreturned tool and its age; confirm with `csift show @<session> --turn -1 --format json` that no `tool_result` carries that `tool_use_id`. Counting rule: one unreturned use = a `tool_use` id with no matching `tool_use_id` later in file order inside the tail window. The unit test `tail_shape_reads_pairing_stop_reason_and_holds_torn_tails` in src/live/tests/surfaces.rs pins the pairing.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "SPEC.md section 6.13; AGENTS.md section 3.9; src/live/tail.rs:3-10 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "From inside a still-running synchronous Bash call: (1) `csift status @trap:<ThreeCamelWords1234> --format json`; (2) in the SAME call, resolve the caller's own transcript and pair its blocks - `python3 - <own transcript>` collecting every `tool_use` id and every `tool_result` `tool_use_id` and printing the ids with no result; (3) dead-lane census over a seeded random sample of 600 files from `find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl`, reading each file's last 512 KB and asking whether its tail carries an unreturned use.",
"observed": "(1) verdict row `{\"age_secs\": 0, \"surface\": \"tail\", \"value\": \"unreturned Bash call\"}`, `tail_state: \"in a Bash call for 0s\"`, verdict `running`. (2) `records: 33 tool_use blocks: 11 tool_result blocks: 10` and `UNPAIRED tool_use (line,name,block keys): [(33, 'Bash', ['caller', 'id', 'input', 'name', 'type'])]` - exactly one unreturned use, the call doing the asking, at the last line. (3) of 600 sampled lanes, 597 had a tail record older than 1 h and 2 of those 597 (0.3%) still carry an unreturned `tool_use` at the tail - the identical on-disk shape with no live process behind it.",
"rule": "One unreturned use = a `tool_use` id with no `tool_result` carrying that id later in file order within the window read. One observation per lane for the dead-lane census; a lane counts as dead when its newest timestamped record is more than 1 h old.",
"note": "Both halves observed directly: the live half in the caller's own lane, the dead-mid-tool half as 2 real lanes whose tail is byte-identical in shape to a live in-flight call. The collision is genuine but rare in this corpus (0.3% of dead lanes), which is why the pid probe rather than the transcript is what separates them. All three code sites in src/live/tail.rs (lines 3-7, 65-67, 93-107) match the current file verbatim; TAIL_WINDOW_BYTES is still `512 * 1024`."
}
]
},
{
"id": "TAIL-002",
"area": "live-tail",
"behavior": "Assistant `stop_reason` is written per API MESSAGE, not per record: on MAIN transcripts it is effectively always present (135 null out of 133,624 assistant records = 0.10% corpus-wide, per-file median 0.00% and worst file 1.94%), while on SUBAGENT transcripts it is null on 65.16% of assistant records (193,062 of 296,275 across 7,515 lanes; per-lane quartiles 58.8% / 64.8% / 71.0%), because a subagent flushes per content block and only the message's terminating record carries the field.",
"depends": "csift's `tail_shape` takes `last_stop_reason` from the newest assistant record only, trusts `end_turn` as the settled shape on the MAIN lane, and on a child lane uses it only as the SECOND conjunct of `generating`; keying on a null mid-message value alone would misclassify most subagent lanes.",
"code": [
{
"path": "src/live/tail.rs",
"lines": "24-26",
"snippet": " /// The newest assistant record's `stop_reason` (trustworthy on the MAIN lane;\n /// null is NORMAL mid-message on subagents).\n pub(crate) last_stop_reason: Option<String>,"
},
{
"path": "src/live/tail.rs",
"lines": "76-80",
"snippet": " if shape.last_stop_reason.is_none() && rec.r#type.as_deref() == Some(\"assistant\") {\n if let Some(sr) = rec.message.as_ref().and_then(|m| m.stop_reason.as_deref()) {\n shape.last_stop_reason = Some(sr.to_string());\n }\n }"
},
{
"path": "src/live/children.rs",
"lines": "23-29",
"snippet": "pub(crate) struct ChildState {\n pub(crate) session_id: String,\n /// `in-flight` (unreturned tail call) | `generating` (recent tail record and the\n /// last assistant record is not an end_turn - the model is mid-generation; a\n /// paired tail alone proves nothing) | `settled`.\n pub(crate) state: &'static str,\n pub(crate) detail: String,"
}
],
"instrument": "Count `stop_reason` per assistant record on a main transcript and on one of its `subagents/agent-*.jsonl`: `csift search '\"type\":\"assistant\"' @<session> --raw | python3 -c \"import sys,json,collections;print(collections.Counter(json.loads(l).get('message',{}).get('stop_reason') for l in sys.stdin))\"`. Counting rule: one observation per `type:\"assistant\"` record, null counted as its own bucket, main-lane and subagent files counted separately.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "dev session 2026-08-30; AGENTS.md section 1; SPEC.md section 6.13"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Regex census of `\"stop_reason\":(null|\"...\")` on every line containing `\"type\":\"assistant\"`, run separately over the 64 top-level transcripts (`find ~/.claude/projects -maxdepth 2 -name '*.jsonl'`) and the 7,520 subagent transcripts (`find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl`).",
"observed": "MAIN: 133,624 assistant records across the 54 main transcripts that have any; 135 carry `stop_reason: null` = 0.10%; 0 records omit the field. Per-file null rate over the 45 mains with >= 50 assistant records: min 0.00%, p50 0.00%, max 1.94%. SUBAGENT: 296,275 assistant records across 7,515 lanes; 193,062 null = 65.16%; 0 omit the field. Per-lane null rate over the 2,009 lanes with >= 50 assistant records: min 18.2%, p25 58.8%, p50 64.8%, p75 71.0%, max 100.0%.",
"rule": "One observation per `type:\"assistant\"` record; a record counts as null when `stop_reason` is present-and-null or absent (both buckets tracked; absent was 0 everywhere). Main-lane and subagent files counted in separate populations; per-file rates computed only where a file has >= 50 assistant records.",
"note": "The behavior holds and the cited sample figures sit inside the measured distribution, but they named unnamed individual sessions that a stranger cannot re-find. Replaced with whole-corpus rates plus the per-lane quartiles, which are rerunnable from the two find expressions above."
}
]
},
{
"id": "TAIL-003",
"area": "live-tail",
"behavior": "The two lanes flush on different schedules. A SUBAGENT transcript flushes per content block, and its launching record is on disk BEFORE the launched command begins executing (measured 0.000 s). The MAIN conversation writes the whole assistant message as one batch of per-block records at message end, so the tool_use record appears 0.19-2.51 s after its own timestamp (n = 20, p50 about 0.33 s) - and a thinking or text sibling written in that same batch can carry a timestamp up to 9.4 s older. The main tail can therefore still read settled for a second or two after a tool has already started.",
"depends": "csift reads the tail as of what is on disk and never waits; the same flush window is why a first `@trap` marker resolution on the main lane normally misses and a re-run of the same marker resolves.",
"code": [
{
"path": "src/live/tail.rs",
"lines": "22-29",
"snippet": " /// flight (or dead mid-tool; the pid probe disambiguates). `(tool name, use ts)`.\n pub(crate) unreturned_use: Option<(String, Option<String>)>,\n /// The newest assistant record's `stop_reason` (trustworthy on the MAIN lane;\n /// null is NORMAL mid-message on subagents).\n pub(crate) last_stop_reason: Option<String>,\n /// The newest record's timestamp (any type that carries one).\n pub(crate) last_ts_utc: Option<String>,\n /// Records inspected (evidence sizing; 0 = empty/unreadable file)."
},
{
"path": "src/live/tail.rs",
"lines": "65-67",
"snippet": " // Backward walk: result ids seen so far are LATER in file order, so a use id absent\n // from that set is unreturned as of the tail.\n let mut later_result_ids: std::collections::HashSet<String> = std::collections::HashSet::new();"
}
],
"instrument": "Run a command that immediately reads its own transcript's tail: on a subagent lane the launching record is already present, on the main lane it is absent for the first seconds. Counting rule: one observation per launch, measured from the dispatch instant to the record's first presence on disk.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "dev session 2026-08-30; AGENTS.md section 3.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) Subagent lane, self-referential: inside one Bash call, poll the caller's own transcript every 20 ms for a literal marker that appears only in THAT command's text, printing the elapsed time when it lands. (b) Main lane: a 0.1 s poller over the transcripts of every session the registry (`~/.claude/sessions/*.json`) marks `busy`/`shell`, recording for each newly appended record the observation instant minus that record's own `timestamp`.",
"observed": "(a) `own tool_use record visible on disk 0.000s after this command started executing` - the launching record was already flushed before the shell ran. (b) 214 records observed on three live main transcripts (CC 2.1.258, 2.1.257, 2.1.251). Assistant records whose only block is `tool_use` (n = 20): lags 0.19, 0.22, 0.23, 0.23, 0.24, 0.24, 0.25, 0.26, 0.28, 0.37, 0.39, 0.42, 0.43, 0.45, 0.52, 0.93, 1.39, 1.53, 1.91, 2.51 s - p50 about 0.33 s, max 2.51 s. All records of one assistant message land in a single batch at the same observation instant (e.g. `t=63.38` for a thinking + text + tool_use triple), so the earlier blocks of that batch carry older timestamps: over all 45 assistant records the lag runs 0.19 to 9.4 s.",
"rule": "(a) one measurement per launch, from the start of the command's own execution to the first byte-presence of its marker. (b) one measurement per appended record: lag = wall clock at the poll that first saw the completed line, minus the record's own `timestamp`; polling granularity 0.1 s, and the floor across all record types was 0.05 s.",
"note": "The direction holds and the mechanism holds, but the main-lane number is smaller than the ledger's 1-3.4 s: the tool_use record itself landed within 2.51 s in every one of 20 observations, p50 about a third of a second. The 3 s exclusion window the hook recipe uses is still safe, but for a different reason than the ledger states - what is genuinely up to ~9 s old at first sight is the EARLIER block of a batched message, not the tool_use record. Subagent half measured on this session's own lane; main half measured on two other live sessions (2.1.258 and 2.1.257) since a subagent cannot dispatch on the main lane."
}
]
},
{
"id": "TAIL-004",
"area": "live-tail",
"behavior": "Within one subagent lane, consecutive-record time gaps during GENUINE work reach p50 1.4 s, p90 9.7 s, p99 63.4 s and p99.9 317.0 s (n = 477,499 gaps over 7,520 lanes) because a long generation writes nothing for minutes, while abandoned lanes sit far out - p1 of the dead set is 16.3 h and its median 1,438 h. The two populations overlap only in the tail: 0.112% of genuine-work gaps exceed the 300 s window, and the dead-lane age distribution is continuous down to the threshold rather than separated from it by a gap.",
"depends": "csift's `CHILD_RECENT_SECS = 300` is set from that distribution, and a child lane is `generating` only on the CONJUNCTION tail-record age <= 300 s AND last assistant `stop_reason != end_turn`; the retired 15 s window read a lane mid-generation as settled 1 time in 17 (under-counting `live_count`, so `waiting-children` collapsed to `idle-eot`), and the mtime-based `active` state is gone.",
"code": [
{
"path": "src/live/children.rs",
"lines": "14-20",
"snippet": "/// Tail-record age (seconds) under which a settled-looking child is treated as still\n/// GENERATING when its last assistant record is not an end_turn. Measured: intra-lane\n/// record gaps reach p99.9 = 295s during genuine work (a long generation writes\n/// nothing for minutes), while dead lanes sit >= 31h out - four orders of magnitude of\n/// separation, so 300s misses almost no real work and resurrects no dead lane. The\n/// old 15s window read a lane mid-generation as settled 1 time in 17.\npub(crate) const CHILD_RECENT_SECS: i64 = 300;"
},
{
"path": "src/live/children.rs",
"lines": "72-74",
"snippet": " } else if tail_age.is_some_and(|a| a <= CHILD_RECENT_SECS)\n && shape.last_stop_reason.as_deref() != Some(\"end_turn\")\n {"
}
],
"instrument": "Over a corpus of `subagents/agent-*.jsonl`, compute consecutive record timestamp deltas inside each file and the age of each file's tail record, then take percentiles. Counting rule: one gap per adjacent timestamped-record pair WITHIN one file (lanes with fewer than two timestamped records excluded), one age per lane; gap percentiles pooled over gaps, age percentiles over lanes. Age is always measured from the tail RECORD timestamp, never file mtime. The unit test `children_report_generating_needs_recency_and_no_end_turn` in src/live/tests/surfaces.rs pins the conjunct.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger item 4; CHANGELOG 0.9.4; AGENTS.md section 1; src/live/children.rs:14-20 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Over every file from `find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl`, extract each line's `timestamp`, take consecutive deltas WITHIN each file, and take the age of each file's last timestamped record; then percentiles over the pooled gaps and over the per-lane ages.",
"observed": "477,499 gaps over 7,520 lanes with >= 2 timestamped records: p50 = 1.4 s, p90 = 9.7 s, p99 = 63.4 s, p99.9 = 317.0 s, p99.99 = 1048.3 s, max = 58,692.1 s. 533 gaps (0.112%) exceed 300 s; 6.0% exceed 15 s. Lane tail-record age: 9 lanes at or under 300 s (all genuinely live at the time of the scan), 7,511 above it with min 0.10 h, p1 16.28 h, p50 1,438 h.",
"rule": "One gap per adjacent timestamped-record pair WITHIN one file (lanes with fewer than two timestamped records contribute no gaps); one age per lane, always from the tail RECORD timestamp, never file mtime. Gap percentiles pooled over gaps, age percentiles over lanes.",
"note": "p50 and p90 reproduce exactly; p99 and p99.9 have drifted up (61.1 -> 63.4 s, 295.4 -> 317.0 s) as the corpus grew from 467,244 to 477,499 gaps, so p99.9 now sits ABOVE the CHILD_RECENT_SECS = 300 constant rather than just below it - the window misses about 1 genuine-work gap in 900. Two other wording corrections: the abandoned-lane p1 is 16.3 h in the present corpus, not 30.9 h (more recent lanes have accumulated), and 'four orders of magnitude of separation' describes the bulk of the two populations, not a gap in the distribution - the minimum dead-lane age IS the threshold. The retired 15 s window is vindicated precisely: 6.0% of gaps exceed 15 s = 1 in 16.7, which is the code comment's '1 time in 17'. Both code sites in src/live/children.rs (14-20, 72-74) match the current file verbatim."
}
]
},
{
"id": "TAIL-005",
"area": "live-tail",
"behavior": "Most DEAD subagent lanes do not end at an `end_turn`: 5,337 of 7,320 lanes with no further activity (73%) carry a last assistant `stop_reason` other than `end_turn`, so a rule keyed on `stop_reason` alone would report nearly three quarters of them as still running.",
"depends": "tail-record recency is therefore the load-bearing conjunct of csift's `generating` state - the `<= 300 s` age test is what keeps those 73% classified `settled`.",
"code": [
{
"path": "src/live/children.rs",
"lines": "72-78",
"snippet": " } else if tail_age.is_some_and(|a| a <= CHILD_RECENT_SECS)\n && shape.last_stop_reason.as_deref() != Some(\"end_turn\")\n {\n // A paired tail with a fresh record and no end_turn = mid-generation (the\n // model is thinking/writing between tool calls; stop_reason alone would\n // mark 73% of DEAD lanes live, so recency is the load-bearing conjunct).\n ("
}
],
"instrument": "Over the abandoned-lane set, count lanes whose newest assistant record carries a `stop_reason` other than `end_turn`. Counting rule: one observation per lane, taken from that lane's newest assistant record only.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "CHANGELOG 0.9.4; AGENTS.md section 1; src/live/children.rs:72-84 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Same subagent-corpus pass as TAIL-004: for each lane take the newest `type:\"assistant\"` record's `stop_reason`, keep the lanes whose newest timestamped record is more than 1 h old, and bucket.",
"observed": "7,476 dead lanes with at least one assistant record: `tool_use` 3,190, `end_turn` 1,987, null 1,703, `stop_sequence` 596. Non-end_turn = 5,489 = 73.4%. Repeating with csift's own settled boundary (tail older than 300 s) gives 5,519 of 7,506 = 73.5%.",
"rule": "One observation per lane, taken from that lane's newest assistant record only; a null `stop_reason` counts as non-end_turn (it is exactly the case a stop_reason-only rule would misread).",
"note": "Reproduces the ledger figure almost exactly (73.4% of 7,476 versus the recorded 73% of 7,320; the population grew with the corpus). The largest single bucket is `tool_use` (3,190 lanes), i.e. lanes that died between a tool call and its result - the shape TAIL-001 covers. The code site src/live/children.rs:72-78 matches verbatim, and the conjunct it guards is what keeps these 73% out of the `generating` state."
}
]
},
{
"id": "TAIL-006",
"area": "live-tail",
"behavior": "A MAIN transcript GROWS while its session is idle - queue enqueues, `attachment` records and metadata lines are appended with no model turn - so file mtime or byte growth alone is never evidence of activity.",
"depends": "csift classifies WHAT landed rather than whether the file moved: `tail_shape` reads record shape and ignores mtime, and `wait` folds each newly appended record through `classify` instead of treating growth as an event; an mtime-based liveness heuristic would call every idle main session running.",
"code": [
{
"path": "src/live/tail.rs",
"lines": "9-10",
"snippet": "//! F9: growth alone is never activity - a main transcript grows while idle (enqueues,\n//! attachments). This module classifies WHAT the tail is, not whether the file moved."
},
{
"path": "src/live/activity.rs",
"lines": "23-27",
"snippet": " pub(crate) fn fold(&mut self, rec: &Record, lane: &str) {\n self.records += 1;\n self.lanes.insert(lane.to_string());\n // A pulse absorbed mid-turn is a queue enqueue line / a queued_command\n // attachment, never a labeled record (v0.10.2): count those deliveries too, so"
}
],
"instrument": "Leave a session idle and enqueue a message, or let another session's background task complete into it: the file grows (`stat` mtime and byte length advance) while `csift status @<session> --format json | jq .verdict` stays idle. Counting rule: compare the byte delta over a fixed idle interval against the verdict, then `csift search '' @<session> --turn -1 --count-by label` to show no new agent records.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "SPEC.md section 6.13; AGENTS.md section 1; src/live/tail.rs:9-10 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) For each of the 64 transcripts from `find ~/.claude/projects -maxdepth 2 -name '*.jsonl'`, read the last 2 MB, parse the complete lines, and report the type of the final record, the records that follow the last `type:\"assistant\"` record, and the seconds between the last assistant record and the newest trailing timestamped record. (b) The live 0.1 s poller of TAIL-003, classifying every newly appended record by type.",
"observed": "(a) 63 of 64 main transcripts end on a NON-assistant record: `last-prompt` 40, `system` 8, `permission-mode` 8, `bridge-session` 4, `cost-state` 2, `file-history-snapshot` 1 - only 1 ends on an assistant record. 53 of 64 carry timestamped records after the last assistant record; those trailing records are `attachment` 203, `system` 138, `user` 87, `last-prompt` 57, `file-history-snapshot` 31, `permission-mode` 24, `mode` 23, `ai-title` 22, `queue-operation` 10, `agent-name` 9, `bridge-session` 6, `cost-state` 3, `atis-latch` 3. Time from the last assistant record to the newest trailing record: p25 1 s, p50 184 s, p75 195 s, max 1,381,853 s (16 days). (b) During a 7-minute live poll of two busy main transcripts, 45 `attachment` records plus `last-prompt`, `ai-title`, `agent-name`, `mode`, `permission-mode`, `atis-latch`, `bridge-session`, `queue-operation` and `file-history-snapshot` lines were appended.",
"rule": "One row per main transcript for the final-record and trailing-record census; one row per appended record for the live poll. 'Growth without a turn' = a record appended after the last assistant record of the file.",
"note": "Confirmed twice over, statically and live. The static form is the sharper instrument: a main transcript's LAST write is almost never a model turn (63/64), and the file keeps growing for a median of 3 minutes - in one case 16 days - after the model's final record, so mtime or byte length says nothing about whether a turn is running. Incidental: this census surfaces three record types absent from the AGENTS.md 3.2 list - `bridge-session`, `cost-state` and `atis-latch` - all metadata-only and all currently ignored by the scanning surfaces. Both code sites (src/live/tail.rs:9-10, src/live/activity.rs:23-27) match verbatim."
}
]
},
{
"id": "TAIL-007",
"area": "live-tail",
"behavior": "A CHILD transcript grows only from the child's own message flow - completion notifications are appended to the MAIN transcript only - so on a child lane record recency IS a real liveness signal, unlike on the main lane.",
"depends": "csift's child-lane state machine may legitimately use recency (the `generating` state), while the main-lane verdict never does.",
"code": [
{
"path": "src/live/children.rs",
"lines": "8-10",
"snippet": "//! (an unreturned tool call) plus recent growth (child transcripts grow only from the\n//! child's own message flow - notifications land in MAIN only, so recency is a real\n//! signal here, unlike the main lane's F9 trap)."
}
],
"instrument": "While a subagent runs, watch both files: `csift status @<session> --format json | jq '.children[] | {session_id, state, detail}'` reports the child by its own tail age, and the main file receives the `<task-notification>` the child file never carries. Counting rule: notification records per file, main versus each subagent transcript.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "AGENTS.md section 1; src/live/children.rs:1-10 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift's own classifier, per project directory: `csift search '' ~/.claude/projects/<ENCODED> -t harness.notification --count-by session --format json`, splitting the census keys into uuid-shaped (main transcript) and non-uuid (subagent transcript) - run over two unrelated project directories. Cross-checked with a raw byte census of `<task-notification>` over the same directory split by file class, and with a parse of every notification-bearing subagent line asking whether the tag sits at content start or mid-prose.",
"observed": "Project directory A: 7 transcripts carrying `harness.notification` records, 183 records, all 7 keys uuid-shaped, 0 subagent. Project directory B: 9 transcripts, 816 records, all 9 uuid-shaped, 0 subagent. Raw bytes in directory A: 1,559 `<task-notification>` hits across 22 of 23 main files versus 706 hits across 81 of 391 subagent files - but all 708 parsed subagent occurrences are `embedded-in-prose`, 0 at content start (record shapes: 519 user/user, 188 assistant/assistant, 1 isMeta).",
"rule": "One census row per transcript; a census key that is not uuid-shaped is a subagent transcript. For the raw cross-check, one observation per line containing the tag, classified content-start versus embedded by testing whether the reconstructed message text begins with the tag.",
"note": "The classifier is decisive: 999 delivered notification records over two project directories, 100% in main transcripts, 0 in child lanes. The raw byte count looks like a counter-example until it is parsed - every subagent hit is a quotation of the tag inside prose (this corpus contains sessions that discuss the tag), which is exactly the section-boundary distinction the classifier applies. Growth in a child lane therefore still means the child itself wrote something. Code site src/live/children.rs:8-10 matches verbatim."
}
]
},
{
"id": "TAIL-008",
"area": "live-tail",
"behavior": "Each workflow run's `journal.jsonl` (one per `wf_*` directory) is written INCREMENTALLY - `{type:\"started\", agentId}` at spawn and `{type:\"result\", agentId, result}` at return - so for a RUNNING run `started` minus `result` is the number of that run's agents in flight (measured live: 57 started, 49 returned, 8 in flight). It is not a liveness test on its own: a killed, failed or abandoned agent never writes a `result`, so the imbalance is permanent - 613 of the 621 unbalanced entries in this corpus belong to journals untouched for 15 h to 77 days. The journal also carries a third type, `failed`, which is neither counted.",
"depends": "csift's `children_report` sums that difference across a session's journals as `journal_in_flight`; an imbalance is a LIVE signal precisely because a terminal dump would always balance.",
"code": [
{
"path": "src/live/children.rs",
"lines": "4-7",
"snippet": "//! meta), and workflow RESULT files are terminal-only - but `journal.jsonl` inside each\n//! `wf_*` dir is written INCREMENTALLY (`{type:\"started\",agentId}` at spawn,\n//! `{type:\"result\",agentId}` at return), so `started - result` = workflow agents in\n//! flight right now."
},
{
"path": "src/live/children.rs",
"lines": "104-105",
"snippet": " // Workflow journals: started - result per wf dir (incremental, so an imbalance is a\n // LIVE signal - a terminal dump would always balance)."
},
{
"path": "src/live/children.rs",
"lines": "126-130",
"snippet": " match v.get(\"type\").and_then(serde_json::Value::as_str) {\n Some(\"started\") => started += 1,\n Some(\"result\") => resulted += 1,\n _ => {}\n }"
}
],
"instrument": "`rg -o '\"type\":\"(started|result)\"' <a wf_*/journal.jsonl> | sort | uniq -c`. Counting rule: one event per journal line, per type; started minus result equals the agents in flight for that run.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.0",
"source": "SPEC.md section 6.13"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "`find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 cat` piped to a python type/key-set census; then per journal `rg -c '\"type\":\"started\"'` minus `rg -c '\"type\":\"result\"'` paired with `stat -f %m` age; then `csift status @<8-char prefix of a long-idle session> --format json`; then the file listing of the one workflow directory whose journal was written in the last hour.",
"observed": "160 journals, 12,754 lines: `started` 6,687, `result` 6,066, `failed` 1. Exactly two key sets: {agentId, key, type} 6,688 and {agentId, key, result, type} 6,066. Corpus-wide started minus result = 621. 124 journals balance at 0; 36 carry a positive imbalance, and 35 of those 36 - 613 of the 621 - were last written 15 h to 1,843 h ago (77 days). `csift status` on a session whose tail record is 58 d 19 h old prints evidence `children: 3513 lane(s), 359 live (359 workflow agent(s) in flight)` and verdict `waiting-children`. The single journal written within the hour, belonging to a run that was actually executing, reads started 57 / result 49.",
"rule": "One event per journal line, per `type`; started minus result per journal directory. A journal counts as stale when its file mtime is more than 1 h old.",
"note": "The mechanism holds and was confirmed live, but the inference 'an imbalance is a LIVE signal precisely because a terminal dump would always balance' is refuted: 98.7% of the corpus-wide imbalance sits in long-dead runs. The consequence is a live csift defect, reproducible in one command - `csift status` on a session whose newest record is 58 days old reports 359 workflow agents in flight and returns the verdict `waiting-children`. src/live/children.rs:104-105 and 126-130 match verbatim; the `_ => {}` arm at 129 is what drops the `failed` type, so a failed agent inflates `journal_in_flight` by one forever. A fix would need to age the journal (or its run) the way child lanes are aged, and to count `failed` as terminal."
}
]
},
{
"id": "TAIL-009",
"area": "live-tail",
"behavior": "A subagent's other on-disk companions carry no usable live state: across 7,520 `subagents/*.meta.json` files there are 15 key sets and not one status/state/completion key (a single file carries `stoppedByUser: true`, a terminal flag, and nothing anywhere reports 'running'). A workflow run's result is terminal-only in both places it lands - as a `result` line appended to the journal at return, and as a run-level result document in the session's `workflows/` directory that does not exist at all until the run ends (163 documents, all with a terminal status: completed 154, failed 5, killed 4).",
"depends": "csift never derives child liveness from a meta or result file - it reads the child's own transcript tail plus the incremental workflow journal.",
"code": [
{
"path": "src/live/children.rs",
"lines": "3-4",
"snippet": "//! `subagents/*.meta.json` has NO status field (child liveness can never come from\n//! meta), and workflow RESULT files are terminal-only - but `journal.jsonl` inside each"
}
],
"instrument": "`jq 'keys' <a subagents/*.meta.json>` shows no status key, and a run in flight has no result file yet. Counting rule: one key-set inspection per meta file; presence/absence per run directory.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.0",
"source": "SPEC.md section 6.13"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "`find ~/.claude/projects -path '*/subagents/*' -name '*.meta.json' -print0 | xargs -0 -n 200 python3 -c '<key-set census + any key matching status/state/complete>'`; then `find ~/.claude/projects -type d -name 'wf_*' -exec ls {} \\;` normalised to file shapes; then a status census of the run-level result documents under each session's `workflows/` directory; then the directory listing of the one run that was executing.",
"observed": "7,520 meta files, 15 distinct key sets, and 0 keys anywhere matching status/state/complete. Largest sets: agentType|spawnDepth 4,582, agentType 2,105, agentType|description|toolUseId 294, agentType|description|spawnDepth|toolUseId 198, the 11-key teammate set 155. ONE exception exists: a single meta with keys agentType|description|isFork|name|spawnDepth|stoppedByUser carrying `\"stoppedByUser\": true`. Every `wf_*` directory contains only `agent-<hex>.jsonl` + `agent-<hex>.meta.json` + `journal.jsonl` (6,687 / 6,687 / 160 corpus-wide) - no result file. The run-level result documents live one level up in the session's `workflows/` directory: 163 of them, status `completed` 154, `failed` 5, `killed` 4, i.e. terminal without exception - and the run that was actually executing had none yet (`ABSENT`) while its journal already showed 57 started / 49 returned.",
"rule": "One key-set observation per meta file; one filename-shape observation per file inside a `wf_*` directory; one status observation per run-level result document; presence/absence checked once for the single in-flight run.",
"note": "Both clauses hold; two wordings needed correcting. First, 'has no status field at all' is true for 7,519 of 7,520 metas - the exception is a `stoppedByUser` flag, which is still terminal-only and still never says 'running', so it cannot ground liveness either. Second, there is no result FILE inside a `wf_*` directory; the terminal-only artefact is the run-level document one level up, and its absence for the currently-running run is the positive test that it is written at return. Code site src/live/children.rs:3-4 matches verbatim."
}
]
},
{
"id": "TAIL-010",
"area": "live-tail",
"behavior": "A pending tool approval leaves NO trace in the transcript: on disk a blocked lane is just the assistant's `tool_use` record (`stop_reason:\"tool_use\"`) sitting as the LAST record with no following `tool_result` for its id, and the block carries only `{type, id, name, input, caller}` - measured as the single key set of all 20,133 tool_use blocks in two project directories, with no `permission`, `isEscalated` or `requires_approval` key anywhere and neither name present in the binary at all. Escalation-blocked, awaiting-execution and wedged therefore share ONE transcript signature. They do not share one machine signature: the session registry row does carry the block - Claude Code sets `{status:\"waiting\", waitingFor:<reason>}` on `~/.claude/sessions/<pid>.json` while a dialog is open (a permission prompt, a plan approval, a question, a sandbox request), a transition write present in every installed version back to 2.1.229.",
"depends": "csift's `lifecycle` detects the frozen lane from the NEWEST meaningful record and forces `Running` (never `completed`, which the old walk-back to a trailing assistant text produced), classifying `escalation-blocked` only when Claude Code's own dangerous-rm rule would hoist the command and reporting `awaiting-execution` otherwise rather than pretending to tell slow from wedged.",
"code": [
{
"path": "src/subagent/lifecycle.rs",
"lines": "133-137",
"snippet": " // Prefer a tool_use CC would hoist (dangerous rm) so classification is escalation-blocked.\n if command\n .as_deref()\n .is_some_and(crate::bash_danger::is_dangerous_rm)\n {"
},
{
"path": "src/live/tail.rs",
"lines": "3-7",
"snippet": "//! Reads the FINAL window of a transcript (bounded, never the whole file), walks it\n//! backward, and reports the liveness-relevant shape: the newest UNRETURNED tool call\n//! (a use whose id has no later result = a tool in flight, or a process dead mid-tool),\n//! the last assistant `stop_reason`, and the last record's instant. A record is only\n//! trusted from a COMPLETE line (torn tails are skipped by the newline framing)."
}
],
"instrument": "With a permission prompt open in a live session, run `csift status @main` from another shell: the verdict must be the human-in-the-loop one, naming the pending tool. `csift show @main --turn -1 --raw | jq -r '.message.content[] | select(.type==\"tool_use\") | keys | join(\",\")'` must list only those five keys, and `rg -c 'isEscalated|requires_approval' ~/.claude/projects/**/*.jsonl` must be 0. Counting rule: one verdict per status run; raw occurrence count for the grep.",
"located": {
"claude_code": null,
"csift": "0.7.0",
"source": "AGENTS.md section 3.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Key census of every `tool_use` block across two unrelated project directories (`find ~/.claude/projects/<ENCODED> -maxdepth 1 -name '*.jsonl'` piped to a python block walk); a recursive structural walk asking whether `isEscalated` ever appears as a JSON KEY rather than inside message text; `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'isEscalated|requires_approval|\"caller\"|permissionRequest'` and `rg -o '.{0,110}waitingFor.{0,110}'`; the same `waitingFor` count across the five Claude Code versions installed here; and `rg -o 'waitingFor|\"status\":\"[a-z]+\"' ~/.claude/sessions/*.json`.",
"observed": "20,133 `tool_use` blocks (7,889 + 12,244) and exactly ONE key set: `caller|id|input|name|type`. The structural walk found 0 occurrences of `isEscalated` as a key; all 38 line hits in one project directory are inside message text (3 user, 9 assistant records in the sampled files). Binary 2.1.258: 0 hits for `isEscalated`, 0 for `requires_approval`, 8 for `\"caller\"`, 15 for `permissionRequest` (all hook-decision plumbing). But the same binary carries `function XBe(T){let I=o_o(T);if(I!==void 0)return{status:\"waiting\",waitingFor:I,working:!1};return{status:T.isLoading||T.delegatedActive?\"busy\":\"i` and the registry write `l$e({status:Nne,waitingFor:oUe},tUe)`, with a dialog table of `waitingFor:\"dialog open\"` / `waitingFor:\"input needed\"` / `waitingFor:\"sandbox request\"` entries and `needs:` strings such as `needs:\"choose: allow or deny the computer-use action\"`; the registry reader is `status:Ke(o.status),waitingFor:typeof o.waitingFor===\"string\"?o.waitingFor:void 0`. `waitingFor` is present in all five installed versions (2.1.229, 2.1.241, 2.1.251, 2.1.257, 2.1.258: 7-8 string hits each, 2 `status:\"waiting\"` each). On disk right now the 7 registry rows read `status` busy 2 / idle 4 / shell 1, none `waiting`, none carrying `waitingFor`.",
"rule": "One observation per `tool_use` block for the key census; one occurrence per structural key for the walk; raw string-hit counts for the binary greps, per version.",
"note": "The transcript half is confirmed exactly and at scale. The clause 'it is in NEITHER transcript NOR any control file' is wrong for the current binary and, judging by the version sweep, was already wrong when it was written: the registry advertises the block. csift itself already models this - src/live/registry.rs:5-8 documents the closed status set `busy | shell | idle | waiting` with `waiting` explicitly covering a permission prompt, and src/live/verdict.rs:296-298 uses `registry_waiting` as one leg of the `waiting-hitl` verdict - so the ledger sentence contradicts csift's own current model rather than describing it. What a live instrument would still decide: whether a tool-permission prompt specifically maps to the `waitingFor:\"dialog open\"` path (the binary's `needs:` table names computer-use, sandbox and several dialog kinds but no plain tool-permission string). To settle it, open a permission prompt in an interactive session and read that session's `~/.claude/sessions/<pid>.json` for `status` and `waitingFor`. Both code sites (src/subagent/lifecycle.rs:133-137, src/live/tail.rs:3-7) match verbatim."
}
]
},
{
"id": "TAIL-011",
"area": "live-tail",
"behavior": "A backgrounded call is never an in-flight call on disk: a `Bash` tool_use with `input.run_in_background:true` is answered by its own `tool_result` (`Command running in background with ID: ...`) as soon as the task is registered - measured at 56 ms on a live launch, and never pending in 3423 backgrounded tool_use records corpus-wide (0 pending, 0 orphan). Record-to-record the pairing reads as a median of 0.46 s for a shell and 0.37 s for an async spawn (main lane, one project dir), 86% of shells within 1 s and a tail out to 9.9 s, because the assistant record's own flush latency is inside that difference. Either way the launch pairs long before the background work ends, so the tail reports no pending call for the whole life of that work, and a main-lane unreturned call only ever covers a synchronous tool.",
"depends": "this is why csift 0.10.0 added the whole-file background scan and a seventh verdict: before it, a session idling at `end_turn` with a running background build reported `idle-eot` and satisfied `wait --until stop`; waiting children are likewise derived from child transcripts and workflow journals, never from the main tail.",
"code": [
{
"path": "src/live/background.rs",
"lines": "4-7",
"snippet": "//! - a backgrounded SHELL: a `Bash` tool_use with `input.run_in_background:true`; its\n//! tool_result arrives within milliseconds (\"Command running in background with ID:\n//! <id>. Output is being written to: <path> ...\"), so the tail state machine pairs it\n//! at once - it is invisible to the unreturned-call logic by construction;"
},
{
"path": "src/live/verdict.rs",
"lines": "358-367",
"snippet": " } else if eot_shape && background.open_counted() > 0 {\n notes.push(\n \"the turn ended, but background task(s) have not returned - by design (a dev \\\n server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n the next session start. `--background-since` / `--ignore-background` narrow \\\n what counts; kill a dead one with the tool or the shell\"\n .to_string(),\n );\n Verdict::IdleBackgroundOpen"
}
],
"instrument": "Launch `sleep 600` with `run_in_background`, then run `csift status @<session>`: the tail row reads `no pending call; last stop_reason end_turn` while the verdict is `idle-background-open` with a background shell row. Counting rule: one launch, one status read. The e2e test `an_open_background_shell_is_the_seventh_verdict_with_its_row` in tests/cli/live/background.rs pins it against a fixture.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "dev session 2026-09-02; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) csift search '\"run_in_background\": *true' -t agent.tool.use --count-by pairing (2) a live launch of `sleep 600` with run_in_background, then csift status @trap:<marker> five seconds later (3) python3 walk of one project dir under ~/.claude/projects joining every tool_use block whose input.run_in_background is true to the tool_result block carrying the same tool_use_id, differencing the two records' `timestamp` fields (4) csift status @<id> --no-subagents over all 23 top-level transcripts of that project dir (5) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Command running in background with ID'",
"observed": "(1) '3423 paired' and 'csift: 3423 matched record(s) across 1 pairing key(s)' - one pairing key, zero pending, zero orphan. (2) tool_result text 'Command running in background with ID: bxov2rges. Output is being written to: ...'; tool_use timestamp 09:33:13.389Z, tool_result timestamp 09:33:13.445Z = 56 ms, while the 600 s sleep kept running; the status read five seconds later showed 'bg shell bxov2rges launched ... (5s ago)' and the only unreturned call at the tail was the synchronous csift Bash call then in flight. (3) 430 jsonl files, 429 backgrounded launches, 0 unpaired. main-lane Bash n=287 min 0.077 s median 0.462 s p90 1.328 s max 9.933 s, 247/287 <= 1 s; main-lane async Agent spawn n=63 min 0.318 s median 0.367 s p90 0.826 s max 4.276 s; subagent-lane Bash n=80 min 0.056 s median 0.390 s p90 1.199 s max 4.818 s. (4) verdict census 20 idle-eot / 1 idle-background-open / 1 running / 1 unknown; the idle-background-open row reads exactly 'verdict idle-background-open' + 'tail no pending call; last stop_reason end_turn (3747865s ago)' + one open async-agent bg row launched 59 d 22 h earlier. (5) the binary carries the literal 'Command running in background with ID: ' and its formatter emits four variants keyed on backgroundedByUser / backgroundedToDeliverMessage / timedOutAfterMs / the plain launch.",
"rule": "A launch = one tool_use block with input.run_in_background == true. It is PAIRED if a tool_result block with the same tool_use_id exists anywhere in the same file; latency = result record timestamp minus use record timestamp. Pairing census counted by csift over the whole corpus; latency distribution counted over one project dir (430 jsonl files, top-level plus subagent transcripts, journal.jsonl excluded). Verdict census = one csift status read per top-level transcript.",
"note": "The substantive assertion survived every attempt to refute it: no instrument found a backgrounded launch sitting unreturned at the tail, and the seventh verdict reproduced verbatim on a real transcript ('no pending call; last stop_reason end_turn' with an open background row 59 days old). Only the phrase 'within milliseconds' needed correction - it is exact for the tool round trip (56 ms measured live) but not for the on-disk record delta, whose median is about half a second and whose tail reaches ten seconds. Two additions the claim does not mention, both now instrument-backed. First, the binary's result formatter has four shapes, and three of them describe a call that started SYNCHRONOUS and was converted to a background task later: 'Command was manually backgrounded by user with ID: ...', 'Command was moved to the background (ID: ...) so that a message that arrived while it was running can reach you; it was not interrupted', and 'Command did not complete within its <N>s timeout and was moved to the background (ID: ...)'. The third fired in this very verification run, so a synchronous main-lane call can leave the unreturned-call state by conversion rather than by completion - it still pairs, so the claim's conclusion is unaffected, but the tail's pending window for a slow synchronous tool is bounded by that timeout rather than by the tool. Second, the source comment at src/live/background.rs:5 carries the same 'within milliseconds' wording and would read truer as 'as soon as the task is registered (56 ms measured; a sub-second record delta)'. All five cited code sites exist verbatim at the cited paths and line ranges (src/live/background.rs:4-7, src/live/verdict.rs:358-367), so corrections.code is empty."
}
]
},
{
"id": "TAIL-012",
"area": "live-tail",
"behavior": "Claude Code appends whole records, so complete-line framing is the only guard a reader needs: no transcript was ever left with an unterminated final line (0 of 430 files) and no line was ever left unparseable (0 malformed in 20677 lines censused by csift stats). A partially written final line is possible rather than routine - records reach 2.6 MB and 142 of them exceed 64 KiB, so an append can in principle be observed mid-write - but it was never caught: 0 torn tails in 18238418 samples covering 190 observed appends.",
"depends": "csift's tail reader and `wait`'s incremental cursor both stop at the last newline and hold the torn remainder for the next poll; parsing the torn line would surface a spurious malformed-line count on every live read.",
"code": [
{
"path": "src/live/tail.rs",
"lines": "52-63",
"snippet": " // Complete lines only: a torn final line (no trailing newline) is held, not parsed.\n let mut lines: Vec<&[u8]> = Vec::new();\n let mut pos = 0usize;\n while pos < window.len() {\n match memchr::memchr(b'\\n', &window[pos..]) {\n Some(nl) => {\n lines.push(&window[pos..pos + nl]);\n pos += nl + 1;\n }\n None => break, // torn tail: skip (the next poll re-reads it complete)\n }\n }"
},
{
"path": "src/live/wait.rs",
"lines": "147-152",
"snippet": " while pos < bytes.len() {\n let Some(nl) = memchr::memchr(b'\\n', &bytes[pos..]) else {\n break; // torn tail: hold the cursor here\n };\n let line = &bytes[pos..pos + nl];\n pos += nl + 1;"
}
],
"instrument": "Poll a live transcript's byte length in a tight loop and check whether the final byte is a newline: a torn tail appears transiently. Counting rule: one sample per poll. The unit test `tail_shape_reads_pairing_stop_reason_and_holds_torn_tails` in src/live/tests/surfaces.rs asserts the torn line is neither parsed nor counted.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "src/live/tail.rs:52 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) three zero-sleep polling loops in python3, each reading os.path.getsize plus the final byte (open, seek(-1,2), read(1)) of live transcripts: 56 files with mtime under 2 h for 40 s; one single live transcript for 100 s; the 6 most recently written transcripts of one project dir for 150 s. (2) a single pass over every jsonl in that project dir checking whether the final byte is a newline and recording the longest line. (3) csift stats @<id> @<id> @<id> --format json, summing skipped_lines over the emitted session rows (the whole-file malformed census).",
"observed": "(1) 641368 + 7092154 + 10504896 = 18238418 samples; 59 + 3 + 128 = 190 samples in which the file had grown since the previous sample; torn_tail_samples 0 in all three runs, examples list empty. (2) 430 jsonl files scanned, 0 whose final byte is not a newline; longest single line 2609260 bytes; 142 lines larger than 64 KiB. (3) 10 session rows, 20677 lines, summed skipped_lines 0.",
"rule": "One sample = one (file, poll) pair that successfully read a size and a final byte; a sample counts as TORN when that final byte is not 0x0A, and counts as an observed append when the size differs from that file's previous sample. Malformed census = sum of skipped_lines over csift stats session rows, which validates every line of every scanned file.",
"note": "The half of the claim csift actually depends on is confirmed by instrument and is the half that matters: records land whole, so holding the trailing partial line and re-reading it next poll is sufficient, and a reader that parsed it would be inventing malformed-line counts that the corpus does not contain. The half that says a torn tail is 'a normal transient state' is overstated at the observed rate of 0 in 190 appends. That is not a refutation - the polling loop resolves a file about every 14 microseconds when watching one file and about every 3.5 ms when watching 56, so a single-syscall append of a few-KB record can slip entirely between two samples, and on macOS a write(2) to a regular file is serialized against concurrent reads, which would make the window zero for any record the runtime emits in one call. What would decide it: a syscall trace of the harness process (root fs_usage or dtrace on the writing pid, unavailable to this session) showing whether a record over 64 KiB is emitted as one write(2) or several; several write calls per record means the torn state is real and reachable, one write call per record means the guard is defensive rather than routine. Both cited code sites exist verbatim at the cited paths and line ranges (src/live/tail.rs:52-63, src/live/wait.rs:144-149), so corrections.code is empty; the unit test named in the instrument, tail_shape_reads_pairing_stop_reason_and_holds_torn_tails, is at src/live/tests/surfaces.rs:89 and asserts records_seen == 1 for a fixture whose final line is torn and whose window also holds a garbage line."
}
]
},
{
"id": "TAIL-013",
"area": "live-tail",
"behavior": "The newest turn-opening record at the tail may be a MACHINE trigger rather than typed prose - an automation pulse whose raw content is `<task-notification>` XML - so the newest 'prompt' in a transcript is not necessarily something a human wrote, and its raw form must never be shown verbatim.",
"depends": "csift's `last` section renders `automation_label()` first and falls back to `reconstructed_user_text` only for a genuine prompt, so the last-prompt row prints `[background-command <id> completed] ...` rather than raw XML.",
"code": [
{
"path": "src/live/last.rs",
"lines": "44-53",
"snippet": " if out.user.is_none() {\n // A genuine prompt, or the machine trigger that opened the turn (an\n // automation pulse renders as its label, never the raw XML).\n let text = rec.automation_label().or_else(|| {\n rec.is_genuine_user()\n .then(|| rec.reconstructed_user_text(None))\n .flatten()\n });\n if let Some(t) = text {\n out.user = Some(excerpt(rec.timestamp.clone(), &t));"
}
],
"instrument": "`csift status @<session>` on a session whose last turn was opened by a background-command completion: the last-prompt row reads the label form, not XML. Counting rule: one status read per session. The e2e test `the_last_section_prints_with_only_a_prompt_on_disk` in tests/cli/live/background.rs pins the section.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "src/live/last.rs:44-53 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(1) csift status @<id> --no-subagents over all 23 top-level transcripts of one project dir under ~/.claude/projects, keeping the sessions whose `last` prompt row renders a bracketed label. (2) python3 read of the matching record in one of those transcripts, printing message.role, type, isMeta and the verbatim string content. (3) python3 census of every main-lane jsonl in that project dir counting type user records whose string content starts with the notification tag, bucketed by the record's own `version` field. (4) the same census run against a live subagent transcript and against its parent main transcript, after five background commands completed during this verification run.",
"observed": "(1) three sessions render an automation label as the newest prompt: '[background-command b1irkb1da completed] Background command \"Commit v0.6.10, reinstall, tag, push (full release chain)\" completed (exit code 0)', '[background-command bah3q0myj killed] Background command \"Wait for both suite outputs\" was stopped', and '[subagent stopped] 2 background agents were stopped by the user: \"This is a test of the `csift` CLI tool's `@trap:<m...\", ...'. No raw XML appeared in any last row. (2) the record behind the first is line 14223, type user, role user, isMeta absent, string content beginning '<task-notification>\\n<task-id>b1irkb1da</task-id>\\n<tool-use-id>toolu_...</tool-use-id>\\n<output-file>...</output-file>\\n<status>completed</status>\\n<summary>Background command \"...\" completed (exit code 0)</summary>'. (3) 180 such turn-opening records, oldest 2026-06-26T23:57:40Z stamped version 2.1.191, newest 2026-09-02T03:35:06Z stamped version 2.1.258. (4) 0 in the 126-line live subagent transcript versus 107 in its parent main transcript, newest stamped 2.1.258.",
"rule": "A turn-opening automation pulse = a record with type user, message.role user, whose message.content is a STRING whose first non-space characters are the notification open tag. A session counts as rendering the label form when the first `last` prompt row printed by csift status begins with a bracketed label instead of the tag. One status read per top-level transcript; the version bucket is the record's own version field, not the installed binary.",
"note": "Confirmed on the current Claude Code, not merely historically: the newest such record in this project dir is stamped 2.1.258 and is four hours old at verification time, so the pulse is still written as a plain type user record carrying raw XML as its whole string content. Every csift render of one was the label form; no instrument produced raw XML in a last row. One lane fact the claim does not state and that an implementer should know: this is a MAIN-lane shape only. Five background commands completed inside a subagent lane during this run and each notification reached the model in context, yet the subagent transcript ended with 0 such records against 107 in the parent main transcript - so a reader that expects the pulse in a subagent transcript will find nothing, and background completion is only recoverable from disk via the parent. That does not weaken the claim, whose consequence is a rendering rule; it narrows where the rule is exercised. The cited code site exists verbatim at src/live/last.rs:44-53, so corrections.code is empty; the e2e tests named in the instruments are present, the_last_section_prints_with_only_a_prompt_on_disk at tests/cli/live/background.rs:444 and an_open_background_shell_is_the_seventh_verdict_with_its_row at tests/cli/live/background.rs:26."
}
]
},
{
"id": "TAIL-014",
"area": "live-tail",
"behavior": "The newest records of a live transcript are the only on-disk evidence of what a session is doing, and the newest record can be ANY type that carries a `timestamp`, not just `user`/`assistant`; several bookkeeping line types carry no `timestamp` at all, so what dates the tail is the newest TIMESTAMPED record in file order.",
"depends": "A trailing TIMESTAMPED bookkeeping record does advance the tail instant by design (measured 6.651 s ahead of the newest assistant record), so the tail age must never be read as 'time since the model last did something' nor compared against a role-scoped last_utc. The claim's second half held: `stop_reason` is read separately from the newest assistant record, and the settled lane stayed idle-eot rather than being classified generating.",
"code": [
{
"path": "src/live/tail.rs",
"lines": "27-28",
"snippet": " /// The newest record's timestamp (any type that carries one).\n pub(crate) last_ts_utc: Option<String>,"
},
{
"path": "src/live/tail.rs",
"lines": "73-75",
"snippet": " if shape.last_ts_utc.is_none() {\n shape.last_ts_utc = rec.timestamp.clone();\n }"
}
],
"instrument": "`csift stats`' `last_utc` is NOT the maximum record timestamp over the whole file: stats aggregates only the role-bearing candidate records, so `last_utc` is the maximum over `user`/`assistant` records (measured: it equalled the newest assistant record exactly, 6.651 s behind the file's true newest timestamped record, a `system` line). `line_types` IS a whole-file census. Therefore a status-tail vs stats-last_utc disagreement is normally the DOMAIN difference, not non-monotonicity - in the same corpus the tail-order instant differed from the file maximum in only 1/23 files, by 1 ms. To see non-monotonicity, compare the last timestamped record in file order against max(timestamp) over the same file directly. The jq also needs the null-safe form: `.evidence[]? | select(.surface==\"tail\")` - the literal `.evidence[]` errors on the header line ('Cannot iterate over null').",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/live/tail.rs:20-31 (the `TailShape` fields)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) python scan of ONE project dir under ~/.claude/projects (23 transcripts): per file, the type of the newest record carrying `timestamp` in FILE ORDER, the per-type census of `timestamp` presence, the count of adjacent backward timestamp steps, and whether the last timestamped record equals the file maximum. (2) On the newest live session: `csift status @<id> --format json | jq -c '.evidence[]? | select(.surface==\"tail\")'` compared against `csift stats @<id> --format json --no-subagents` and against a direct python read of the same transcript.",
"observed": "One project dir, 23 transcripts: newest TIMESTAMPED record in file order was `system` in 18 files, `user` in 4, `queue-operation` in 1 - 19/23 were NOT user/assistant. The last LINE of all 23 was a non-record bookkeeping type (last-prompt 19, permission-mode 3, queue-operation 1). Eight line types carried a timestamp on 0 of their occurrences: last-prompt 3543, mode 3520, permission-mode 3520, ai-title 3139, agent-name 2687, file-history-snapshot 435, bridge-session 270, atis-latch 269. Timestamps are not monotone in file order: 22/23 files contained at least one backward adjacent step, 8567 backward pairs over 87444 timestamped records (9.8%); despite that, the last timestamped record differed from the file maximum in only 1/23 files, by 1 ms. Live session (159028 lines): the newest timestamped record was a `system` record at 2026-09-02T11:22:40.203Z and `csift status`'s tail row read age_secs 697 against it; `csift stats` last_utc was 2026-09-02T11:22:33.552Z, exactly the newest `assistant` record, 6.651 s older. The verdict stayed idle-eot with tail value 'no pending call; last stop_reason end_turn'.",
"rule": "One transcript = one observation. (a) type of the newest record bearing a `timestamp` key, scanning lines in file order and keeping the last one that has the key. (b) per line `type`, count occurrences with and without a `timestamp` key. (c) count adjacent pairs (i, i+1) over timestamped records in file order where ts[i+1] < ts[i]. (d) one status-vs-stats comparison per session, both read in the same second.",
"note": "Behavior confirmed on all three points, and more strongly than stated: on this corpus the newest timestamped record is usually NOT user/assistant (19/23 files), and the last physical LINE is almost always a type carrying no timestamp at all. Two line types seen bearing no timestamp are not in the csift docs' enumeration - `bridge-session` (270) and `atis-latch` (269) - which is exactly the tolerance the model is built for, but worth a separate claim. Code sites verified verbatim: src/live/tail.rs:27-28 and 73-75 both match the claimed snippets byte for byte, and `parse::parse_line` (src/parse/lines.rs:113) is type-agnostic, so the reverse walk really does take the instant from any type that carries one."
}
]
},
{
"id": "TAIL-015",
"area": "live-tail",
"behavior": "The written payload of a file tool lives under a DIFFERENT `tool_use` input key per tool - `Write.content`, `Edit.new_string`, `NotebookEdit.new_source` - and `MultiEdit` nests its payload one level down as `edits[].new_string`; there is no common content key, just as the path splits `file_path` versus `NotebookEdit.notebook_path`.",
"depends": "`csift wait --until 'write:<path-regex>[:<line-regex>]'` reads the path from `file_path` or `notebook_path` and scans exactly the string-valued input keys `content`, `new_string` and `new_source`, so a MultiEdit's per-hunk `edits[].new_string` is unreachable to the line half of the condition: such a write can satisfy the path half alone and never a line regex.",
"code": [
{
"path": "src/live/conditions.rs",
"lines": "24-29",
"snippet": " /// A Write/Edit/MultiEdit/NotebookEdit whose path matches (and whose written content\n /// contains a line matching, when given).\n Write {\n path_re: regex::Regex,\n line_re: Option<regex::Regex>,\n },"
},
{
"path": "src/live/conditions.rs",
"lines": "172-182",
"snippet": " } if matches!(n.as_str(), \"Write\" | \"Edit\" | \"MultiEdit\" | \"NotebookEdit\") => {\n let path = input\n .get(\"file_path\")\n .or_else(|| input.get(\"notebook_path\"))\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default();\n if !path_re.is_match(path) {\n return false;\n }\n line_re.as_ref().is_none_or(|re| {\n [\"content\", \"new_string\", \"new_source\"].iter().any(|k| {"
}
],
"instrument": "The unit test at src/live/tests/conditions.rs:143-157 fixtures a `NotebookEdit` tool_use carrying `notebook_path` + `new_source` and asserts the path source is consulted and that a path miss short-circuits before the line regex. Live check: `csift wait @<id> --timeout 60 --until 'write:parse\\.rs:fn main'` is satisfied by an `Edit` of that file and never by a `MultiEdit` of it. Counting rule: one condition evaluation per appended record; the condition is satisfied by the FIRST matching record.",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/live/conditions.rs:24-29 doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(1) `strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc.strings` then `rg -o` for each tool's input schema. (2) Per-project-dir tool census: `csift search '' @<encoded-dir> --no-subagents -t agent.tool.use --count-by tool`, run once per project dir. (3) Python key census over ONE project dir (23 transcripts) of every Write/Edit/MultiEdit/NotebookEdit tool_use `input` key. (4) Four live `csift wait` runs against a synthetic fixture home: `csift wait --claude-home $FIX @<fixture-uuid> --timeout 6 --until '<cond>'`, with a matching tool_use record appended 2 s after the readiness line.",
"observed": "Binary 2.1.258 schema strings, quoted: Write - `file_path:i().describe(\"The absolute path to the file to write (must be absolute, not relative)\"),content:i().describe(\"The content to write to the file\")`; Edit - `file_path:i().describe(\"The absolute path to the file to modify\"),old_string:i().describe(\"The text to replace\"),new_string:i().describe(\"The text to replace it with (must be different from old_string)\")`; NotebookEdit - `notebook_path:i().describe(\"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)\")` and `new_source:i().describe(\"The new source for the cell\")`; MultiEdit nesting - `{file_path:e,edits:n.map(({old_string:f,new_string:_,replace_all:v})=>...)}`. The path-key split is stated outright in the tool table: `Write:{input:\"file_path\",...},Edit:{input:\"file_path\",...},MultiEdit:{input:\"file_path\",...},NotebookEdit:{input:\"notebook_path\",...}`, and all four are in the live tool registry line `[\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",\"Edit\",\"MultiEdit\",\"Write\",\"NotebookEdit\",...]`. Corpus: 1722 Edit tool_use records, all 1722 carrying exactly {file_path, old_string, new_string, replace_all}; 145 Write records, all 145 carrying exactly {file_path, content}; 0 MultiEdit and 0 NotebookEdit records in any of the 15 project dirs. Live wait runs: Edit + `write:target\\.rs:NEEDLE_ALPHA` fired (exit 0); MultiEdit + `write:target\\.rs` (path half alone) fired (exit 0); MultiEdit + `write:target\\.rs:NEEDLE_ALPHA`, with NEEDLE_ALPHA present in edits[0].new_string, did NOT fire - 'fired timeout', exit 124.",
"rule": "One tool_use block = one record for the key census; a record counts under a tool only if block.type=='tool_use' and block.name is that tool. For the wait runs, one condition evaluation per appended record and the first matching record wins; the pass/fail signal is the exit code (0 = a condition fired, 124 = timeout) plus the `fired` field.",
"note": "The blind spot reproduces exactly as claimed, and the three-run design isolates it: the same needle in a top-level `new_string` fires while the same needle one level down in `edits[].new_string` does not, and the MultiEdit record is not being ignored wholesale because its path half fires on its own. Two side observations. First, MultiEdit and NotebookEdit have zero on-disk instances across all 15 project dirs here, so the blind spot is real at the schema level but has never been exercised by this corpus - the reproduction above is a synthetic fixture, not a found record. Second, `csift wait --help` describes the condition as 'a Write/Edit whose path matches', narrower than the code, which matches Write|Edit|MultiEdit|NotebookEdit (src/live/conditions.rs:161); a reader of the help would not expect a MultiEdit to satisfy the path half at all. Code sites verified verbatim at src/live/conditions.rs:24-29 and 161-171."
}
]
},
{
"id": "TAIL-016",
"area": "live-tail",
"behavior": "The set of transcript files that constitute one session is NOT fixed at its start: a subagent lane is born mid-session - its `subagents/agent-<hex>.jsonl` exists only from the moment the lane is spawned - so a watcher that enumerated the file set once would never see a lane spawned after it started.",
"depends": "`csift wait` re-enumerates a session's subagent transcripts on EVERY poll and joins a newly discovered lane at offset 0, which is exact because a file born after start is wholly post-start; that is what keeps its strict post-start baseline honest for lanes it did not see at start.",
"code": [
{
"path": "src/live/wait.rs",
"lines": "98-112",
"snippet": " // ── New-child discovery: a lane spawned after start joins the watch set with\n // baseline 0 (its whole content is post-start). ──\n if args.want_subagents() && !is_subagent_target {\n for sub in crate::subagent::subagent_transcript_files(&main).unwrap_or_default() {\n if !cursors.iter().any(|c| c.path == sub) {\n let lane = crate::subagent::session_id_from_path(&sub);\n cursors.push(Cursor {\n path: sub,\n offset: 0,\n is_main: false,\n lane,\n });\n }\n }\n }"
}
],
"instrument": "Start `csift wait @<id> --timeout 120 --format json` against a session that then spawns a subagent: the activity census must list the new lane. Counting rule: one lane per discovered subagent transcript path in the census; a lane joined mid-wait contributes its whole file, every byte of which is post-start by construction.",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/live/wait.rs:95-109"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(1) Python stat walk scoped to ONE project dir under ~/.claude/projects: for every `<session>/subagents/**/agent-*.jsonl`, its filesystem birthtime minus the parent transcript's FIRST timestamped record. (2) Live `csift wait --claude-home $FIX @<fixture-uuid> --timeout 7 --until 'tool:Bash' --format json` against a fixture session that had NO subagents directory at start, with `subagents/agent-<hex>.jsonl` created 2 s after the readiness line; plus the same run with `--no-subagents` as a negative control.",
"observed": "Scoped project dir: 5497 subagent transcripts compared; 5497/5497 were born strictly after their parent session's first timestamped record, 0 at or before it; gap seconds min 633.6, median 1981488, max 3350868. Live run: stderr readiness line said `csift: watching 1 file(s) from byte offsets; conditions: tool:Bash; timeout 7s` - the lane did not exist at baseline - and after the lane was created mid-wait the run exited 0 with `{\"fired\":\"tool:Bash\",\"activity\":{\"lanes\":1,\"records\":1,\"tools\":{\"Bash\":1},...}}`. Negative control with `--no-subagents`, identical lane birth: 'fired timeout', exit 124.",
"rule": "One subagent transcript = one observation; born-after is birthtime > parse(parent's first record timestamp). For the live run, the readiness line's file count is the baseline watch-set size and the `activity.lanes` count is the number of distinct lanes that contributed post-baseline records; exit 0 with a `fired` condition means the newly discovered lane's record was actually read.",
"note": "Both halves are instrument-confirmed. The negative control matters: with `--no-subagents` the identical lane birth produced a timeout, so the exit-0 in the positive run is attributable to the per-poll re-enumeration and not to anything landing in the main transcript. The baseline-exactness argument also checks out in code: initial cursors are seeded from `std::fs::metadata(...).map(|m| m.len()).unwrap_or(0)` (src/live/wait.rs:45-70) while a lane discovered inside the loop joins at offset 0, and a file that did not exist at baseline cannot hold pre-baseline bytes. Code site verified verbatim at src/live/wait.rs:95-109."
}
]
},
{
"id": "TAIL-017",
"area": "live-tail",
"behavior": "The sidecar FILE is born at the first pending ask; the DIRECTORY usually is not. `<session-uuid>/` is the session's shared sidecar dir - the same dir that holds `subagents/` - so Claude Code creates it on the first subagent spawn, independent of any elicitation. Measured: of 20 sidecar files, 13 sat in a dir that predated them by more than 1 s (median 118410 s, about 1.4 days; max 775906 s, about 9 days), and 11 of those dirs also held `subagents/`; only 7 had a dir born with the file. Since `elicitation::sidecar_path` resolves on `dir.is_dir()` alone (src/subagent/discover.rs:9-14), the path can therefore resolve long before any ask has occurred, at which point the sidecar file simply does not exist yet. Absence at start remains the normal state and never an error, but the correct statement is: the path may or may not resolve at start, and the FILE appears at the first pending ask.",
"depends": "`csift wait` re-attempts sidecar path resolution on every poll instead of resolving once at start, and joins the file at offset 0 when it appears (wholly post-start by construction); a one-shot resolution would miss every elicitation that began after the watch, which is exactly the class of event a wait exists to catch.",
"code": [
{
"path": "src/live/wait.rs",
"lines": "113-129",
"snippet": " // ── Sidecar discovery: the sidecar DIR is typically born mid-wait (the first\n // pending ask creates it), and its path resolves only once the dir exists -\n // so re-attempt each poll. A file born after start is wholly post-start, so\n // baseline 0 is exact. ──\n if !is_subagent_target {\n if let Some(sc) = crate::elicitation::sidecar_path(&main) {\n if !cursors.iter().any(|c| c.path == sc) {\n let lane = crate::subagent::session_id_from_path(&main);\n cursors.push(Cursor {\n path: sc,\n offset: 0,\n is_main: true,\n lane,\n });\n }\n }\n }"
}
],
"instrument": "With the elicitation hook recipe installed, watch a session with `csift wait @<id> --timeout 120 --format json` and let it raise an AskUserQuestion: the sidecar file beside the session's `subagents/` directory does not exist before that ask and does from it, and the pending marker lands in the wait census. Counting rule: one observation per elicitation - the sidecar file's existence before versus after the first pending ask; a session that never elicits never grows one.",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/live/wait.rs:110-126"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) Python walk over the session sidecar directories under ~/.claude/projects: how many `<session>/` dirs exist, how many hold `subagents/`, how many hold `elicitations.jsonl`, and for each sidecar file its birthtime minus its containing dir's birthtime. (2) `python3 -c` read of ~/.claude/settings.json to confirm the recording hook is installed. (3) Live `csift wait --claude-home $FIX @<fixture-uuid> --timeout 7 --until 'auq' --format json` against a fixture session whose `<uuid>/` dir did NOT exist at start, with the dir and a `csiftPhase:\"pending\"` AskUserQuestion marker created 2 s after the readiness line.",
"observed": "Hook is installed: settings.json hook events are ['Elicitation','ElicitationResult','PostToolUse','PostToolUseFailure','PreToolUse','SessionStart','Stop','UserPromptSubmit'] and the hook config mentions the elicitation sidecar. Corpus: 65 session sidecar dirs, 27 holding `subagents/`, 20 holding `elicitations.jsonl`. Of those 20 sidecar files, only 7 sat in a dir born within 1 s of the file; 13 sat in a dir strictly older by more than 1 s (gap seconds min 183, median 118410, max 775906), and 11 of those 13 dirs also held `subagents/`. Live run: the fixture's `<uuid>/` dir did not exist at start, stderr said `csift: watching 1 file(s) from byte offsets; conditions: auq; timeout 7s`, and after the dir plus sidecar file were created mid-wait the run exited 0 with `activity.tools {\"AskUserQuestion\":1}` and 1 record.",
"rule": "One sidecar file = one observation; 'dir created by the first ask' counted as (sidecar file birthtime - containing dir birthtime) <= 1 s. For the live run, the readiness line's file count is the baseline watch-set size (1 = the main transcript only, no sidecar cursor) and exit 0 with a fired condition means the sidecar was discovered and read on a later poll.",
"note": "The depends half is instrument-confirmed and unaffected by the correction: the fixture had neither dir nor file at baseline, wait watched 1 file, and the condition still fired at exit 0 - a one-shot resolution would have missed it. The offset-0 baseline stays exact under the correction, because a pre-existing dir means the sidecar path resolves at seed time and `seed` snapshots the real file length (src/live/wait.rs:45-70), while a path that only resolves later implies the file was created after the baseline. The 'when the recording hook is installed' qualifier is load-bearing and worth keeping prominent: the sidecar is written by a hook, not by Claude Code, so on a machine without it no sidecar ever appears and `--until auq` degrades to native AskUserQuestion tool_use blocks only (src/live/conditions.rs:127-139). Code site verified verbatim at src/live/wait.rs:110-126."
}
]
},
{
"id": "MISC-001",
"area": "misc",
"behavior": "Hook-injected conversation context lands in the transcript as a `type:\"attachment\"` record whose payload is `{\"type\":\"hook_additional_context\",\"content\":[...]}` - the single attachment payload shape that carries injected context. `content` is a string ARRAY in real data (one element per injected block; a bare string is tolerated), holding the text a SessionStart / UserPromptSubmit / ... hook injected.",
"depends": "csift joins the array with a newline in the one extractor `Record::hook_additional_context_text`, classifies the record `harness.meta.hook`, and scans it only under `search --additional-context` (or the `--attachments` superset) with the candidate needle `&&`-gated so a default scan never parses attachment lines; an explicit `show --line`/`--uuid` address renders it flag-free under the refetch law. A bare-string-only reader returns nothing on real data.",
"code": [
{
"path": "src/cli/search_args.rs",
"lines": "493",
"snippet": " /// Also scan hook-injected `additionalContext`: the `attachment` records a SessionStart /"
}
],
"instrument": "`rg -m1 'hook_additional_context' <transcript> | jq '.attachment.content | type'` prints `array`; then `csift search '<a needle only in injected hook context>' --additional-context @<id>` must find it while the same search without the flag must not. Counting rule: one payload per raw attachment line.",
"located": {
"claude_code": "2.1.258",
"csift": "0.7.6",
"source": "SPEC.md section 4.8; SPEC.md section 6 v0.7.6 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -NI '\"hook_additional_context\"' ~/.claude/projects/<one project dir> -g '*.jsonl' | jq -r '.attachment.content|if type==\"array\" then \"array-len-\\(length)\" else \"SCALAR-\\(type)\" end' | sort | uniq -c ; csift search 'project-memory-context' @<id> --count-by label (then the same with --additional-context, then with --attachments) ; csift show @<id> --line 8",
"observed": "Largest such project dir: 25666/25666 attachment lines carry content as a JSON ARRAY, zero scalars; lengths 25541 x len-1 plus len-5, 21, 22, 23, 24, 25, 27, 28, 29 (so multi-block injections are real, not hypothetical). A second project dir: 9459/9459 array. Payload type on the sampled record: {\"type\":\"attachment\",\"att_type\":\"hook_additional_context\",\"content_type\":\"array\"}. Gate, on one needle that lives on exactly 2 attachment lines of one transcript (one hook_success payload, one hook_additional_context payload): default scan = 0 records; --additional-context = 1 record, sole label key harness.meta.hook; --attachments = 2 records, keys harness.meta.hook + harness.meta.attachment (a strict superset). csift show --line 8 rendered the record flag-free as '(gear) harness.meta.hook L8 <project-memory-context> ... </project-memory-context>'.",
"rule": "One payload per raw attachment line; the shape census counts lines, the gate census (--count-by label) counts records, so a two-section record cannot inflate it.",
"note": "Both halves measured. The array shape is universal in the sampled corpus (zero bare strings), so the tolerated bare-string arm is defensive only, and a bare-string-only reader would indeed return nothing. Code sites at src/search/scan.rs:306-309 and src/model/predicates.rs:329-333 are verbatim; the third moved down 5 lines."
}
]
},
{
"id": "MISC-002",
"area": "misc",
"behavior": "Large tool outputs are EXTERNALISED to a sibling `tool-results/<id>.txt`, leaving an inline `<persisted-output>` pointer whose body carries the line `Full output saved to: <ABSOLUTE_PATH>` plus a short preview; the same path is also exposed structurally on `toolUseResult.persistedOutputPath`.",
"depends": "`search --resolve-persisted` replaces the pointer with the file's content BEFORE matching, preferring the structured field (exact, no regex) and falling back to scraping the inline marker line; a read failure is non-fatal and appends an explicit note rather than silently matching nothing.",
"code": [
{
"path": "src/model/record.rs",
"lines": "306-307",
"snippet": " #[serde(rename = \"persistedOutputPath\")]\n pub(crate) persisted_output_path: Option<serde_json::Value>,"
}
],
"instrument": "`rg -c 'persistedOutputPath' ~/.claude/projects/*/*.jsonl` for the population and `ls ~/.claude/projects/*/*/tool-results/ | head` for the store; then `csift search '<a phrase only in the external file>' @<session> --resolve-persisted -c` versus the same without the flag. Counting rule: one pointer per externalised tool result; matched records with and without resolution.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 4.6 as consumed by section 6.2; src/model/exchange.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -NI -m1 'persistedOutputPath' ~/.claude/projects/<dir>/<session>.jsonl | jq -c '.toolUseResult|keys' ; rg -NIo 'Full output saved to: [^\"\\\\]*' <same transcript> ; find ~/.claude/projects -maxdepth 4 -path '*/tool-results/*.txt' | wc -l ; csift search '<a phrase present only in the external .txt>' @<id> -c (then the same with --resolve-persisted)",
"observed": "Structured field present on a real record: toolUseResult keys [\"interrupted\",\"isImage\",\"noOutputExpected\",\"persistedOutputPath\",\"persistedOutputSize\",\"stderr\",\"stdout\"]. Inline marker present on the same session: 'Full output saved to: ~/.claude/projects/<dir>/<uuid>/tool-results/<id>.txt', with 10 marker lines and 7 <persisted-output> blocks in one transcript. Store: 1891 externalised .txt files corpus-wide, 12 tool-results directories in one project dir. Functional gate: a 35-character phrase that rg finds in the .txt file and NOT in the .jsonl matched 0 exchanges without the flag and 1 exchange with --resolve-persisted. Read-failure arm, against a fixture transcript under --claude-home whose pointer names a path that does not exist: output kept the inline text and appended '[csift: could not resolve persisted output /nonexistent/dir/gone.txt: No such file or directory (os error 2)]', exit 0.",
"rule": "One pointer per externalised tool result; the gate counts matched exchanges for one fixed needle with and without resolution (0 vs 1).",
"note": "Both carriers (structured field and inline marker) coexist on the same session, so the 'prefer structured, fall back to scraping' order is exercisable. Code sites at src/model/grouping.rs:249-253 and src/model/exchange.rs:313-316 are verbatim; the record.rs field moved down 6 lines."
}
]
},
{
"id": "MISC-003",
"area": "misc",
"behavior": "Claude Code injects a fixed continuation prompt as a type:\"user\", isMeta record whose message.content is a single-element block array [{\"type\":\"text\",\"text\":\"Continue from where you left off.\"}] - not a bare content string. The '522 occurrences' figure does not reproduce: the live corpus carries 6 such records across 4 sessions.",
"depends": "csift classifies it `harness.schedule.continuation`; being `isMeta` it is excluded from turn opening, so it never counts as an operator message.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "190",
"snippet": "pub const SCHEDULE_CONTINUATION_MARKER: &str = \"Continue from where you left off.\";"
}
],
"instrument": "Count the leaf, not the phrase: `csift search '' -t harness.schedule.continuation --count-by label`. The claim's clause 'hits must key under harness.schedule.continuation only' is wrong - the sentence is ordinary prose that any transcript can quote, and a text search for it keys under 9 labels.",
"located": {
"claude_code": null,
"csift": "0.2.0",
"source": "src/model/markers.rs SCHEDULE_CONTINUATION_MARKER doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search 'Continue from where you left off\\.' --count-by label ; csift search '' -t harness.schedule.continuation --count-by label ; csift search '' -t harness.schedule.continuation --count-by session ; csift search '' -t harness.schedule.continuation --raw | jq -c '{type, isMeta, role: .message.role, content: .message.content}'",
"observed": "Leaf census over the live corpus (7608 sessions in scope): 6 records under harness.schedule.continuation, across 4 distinct sessions. All 6 are byte-identical in shape: {\"type\":\"user\",\"isMeta\":true,\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Continue from where you left off.\"}]}. A plain TEXT search for the phrase matches 409 records across 9 label keys: agent.tool.result 324, agent.tool.use 46, agent.message 13, agent.thinking 8, agent.communication.inbox 7, harness.schedule.continuation 6, harness.compaction.summary 3, agent.communication.sent 2, harness.meta.attachment 2 - and ZERO under user.message.",
"rule": "One record per label key. The leaf count uses an empty pattern with -t <leaf> so prose quoting the phrase cannot contribute; the text search uses the phrase itself.",
"note": "The behavioral core holds under an instrument that ran: the record exists, is type:\"user\" + isMeta, carries exactly that text, and never classifies user.message (0 of 409 phrase matches), so it never counts as an operator message. Only the population number and the phrase-based instrument needed correcting. src/model/markers.rs:177 is verbatim; the 522 figure is also written into that constant's doc comment two lines above, so a doc fix belongs there too."
}
]
},
{
"id": "MISC-004",
"area": "misc",
"behavior": "The two input forms and the absence of any task id both reproduce exactly, and '562 uses across 3 files' is exact by block count (561 by distinct tool_use id). The return shape needs splitting: the ARM form returns exactly {clampedDelaySeconds, scheduledFor, wasClamped}; the STOP form returns a 5-key superset adding {stopped, cancelledWakeups}. Neither form carries an id of any kind.",
"depends": "csift classifies the fired prompt `harness.schedule.wakeup` off that sentinel rather than any task id, and a ScheduleWakeup can never appear in `status`'s background section because there is no id to join on.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "192-193",
"snippet": "/// The `ScheduleWakeup` TIMER's fired-prompt sentinel (GOLD §5) - `harness.schedule.wakeup`.\n/// When a `ScheduleWakeup` tool fires, the harness injects its `prompt`; this fixed sentinel is"
}
],
"instrument": "`csift search 'ScheduleWakeup' . -t agent.tool.use --raw | jq -c '.message.content[]?|select(.name==\"ScheduleWakeup\")|.input|keys' | sort | uniq -c`, then `csift search '' . --count-by label | grep harness.schedule`. Counting rule: one row per ScheduleWakeup tool_use block.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search 'ScheduleWakeup' -t agent.tool.use --raw | jq -c 'select(.message.content!=null) | .message.content[]? | select(.type==\"tool_use\" and .name==\"ScheduleWakeup\") | .input | keys' | sort | uniq -c ; a python pass over the 3 transcripts that carry those blocks, joining each tool_use id to its tool_result carrier and tallying toolUseResult key sets ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'clampedDelaySeconds|wasClamped|delaySeconds' ; csift search '' -t harness.schedule.wakeup --count-by label",
"observed": "562 ScheduleWakeup tool_use blocks (561 distinct tool_use ids - one id appears on two records, the per-block flush) across exactly 3 files, per-file 556/4/2. Inputs: 561 x [\"delaySeconds\",\"prompt\",\"reason\"], 1 x [\"stop\"]. Joined results: 561 x exactly (\"clampedDelaySeconds\",\"scheduledFor\",\"wasClamped\"); the single stop call returns (\"cancelledWakeups\",\"clampedDelaySeconds\",\"scheduledFor\",\"stopped\",\"wasClamped\"). Union of every ScheduleWakeup result key = ['cancelledWakeups','clampedDelaySeconds','scheduledFor','stopped','wasClamped']; keys matching /id|task/i: none. Binary corroboration, verbatim: 'True if the requested delaySeconds was outside [60, 3600]', 'clampedDelaySeconds', 'wasClamped', and '`delaySeconds` and `reason` are required when `stop` is not true.' Result text sample: 'Next wakeup scheduled for HH:MM:SS (in Ns). Nothing more to do this turn - the harness re-invokes you when the wakeup fires or a task-notification arrives.' harness.schedule.wakeup leaf: 1 record corpus-wide.",
"rule": "One row per ScheduleWakeup tool_use BLOCK (562); ids deduped separately (561). Result keys read off the toolUseResult object of the tool_result carrier whose tool_use_id joins the block.",
"note": "The binary's own required-field sentence independently confirms the {delaySeconds, prompt, reason} vs {stop:true} disjunction. The 'can never appear in status's background section' consequence follows from the measured key union carrying no id, which is the join key that section needs. src/model/markers.rs:179-180 is verbatim."
}
]
},
{
"id": "MISC-005",
"area": "misc",
"behavior": "Both directory forms and the string-id/open-status set reproduce exactly, and the merge is proven on a session that owns both. The per-file key set is NOT fixed: activeForm is optional (82 of 402 files lack it) and two further keys occur in real data - owner (36 files) and metadata (26 files).",
"depends": "`csift status` merges both directory forms into one tasks section and renders anything that is not `completed` as an open row with its verbatim status; resolving only one form silently reports no tasks for half the sessions on disk.",
"code": [
{
"path": "src/live/tasks.rs",
"lines": "1-8",
"snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a\n//! per-session directory. Two directory-name forms exist on real disks (both verified):\n//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file\n//! carries `{id, subject, description, activeForm, status, blocks, blockedBy}` with\n//! string ids. The set of `status` values is OPEN (pending / in_progress / completed\n//! observed); anything that is not `completed` renders as an open row with its verbatim"
}
],
"instrument": "`ls ~/.claude/tasks | head` shows both directory forms; then `csift status @<id>` for a session using each. Counting rule: one file per task, directories merged per owner.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "SPEC.md section 6.13"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "ls ~/.claude/tasks | awk '{ if ($0 ~ /^session-[0-9a-f]{8}$/) f=\"session-<8hex>\"; else if ($0 ~ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) f=\"full-uuid\"; else f=\"other\"; print f }' | sort | uniq -c ; find ~/.claude/tasks -name '*.json' -print0 | xargs -0 -n50 jq -sc '.[]|keys' | sort | uniq -c ; jq -sr '[.[]|.status]|group_by(.)|map({(.[0]):length})|add' over both owner dirs of one session ; csift status @<that session>",
"observed": "177 owner directories: 161 in the session-<first 8 uuid chars> form, 16 in the full-uuid form - both forms live on this disk. 402 task files. Key-set census: 258 x [activeForm,blockedBy,blocks,description,id,status,subject]; 82 x the same MINUS activeForm; 32 x + owner; 26 x + metadata; 4 x [blockedBy,blocks,description,id,owner,status,subject]. Types: id string, status string, blocks array. status values across all 402 files: completed 271, pending 99, in_progress 32 - exactly the three the claim names, nothing else. Merge proof: ONE session owns BOTH forms - 7 files under <full-uuid>/ (6 completed + 1 in_progress) and 21 under session-<8hex>/; combined 28 = 18 completed + 1 in_progress + 9 pending. csift status on that session printed 'tasks 10 open ; 18 completed' (10 = 1 in_progress + 9 pending) with each open row carrying its verbatim status word.",
"rule": "One file per task; owner directories merged per session; open = status != completed. The merge is decided by a session whose task files are split across BOTH directory forms, so reading either form alone gives a strictly smaller number.",
"note": "Reading only the full-uuid form for that session would have reported 7 tasks instead of 28, and only the short form 21 - the silent under-report the claim's depends clause predicts. src/live/tasks.rs:1-8 is verbatim; its key list should note that activeForm is optional and that owner/metadata also appear."
}
]
},
{
"id": "MISC-006",
"area": "misc",
"behavior": "A conversation FORK, rewind, retry or parallel lane leaves exactly one plain DAG fact: some record has MORE THAN ONE conversation child (a later `parentUuid` re-attach). Which side is live is NOT computable from the jsonl - parallel tool results share a parent BY CONSTRUCTION, so `user` records carrying a `tool_result` block are not conversation children.",
"depends": "`show --branch-points` reports the facts and ranks them by widest inter-child time gap but never classifies which side is live: a live/abandoned classifier was prototyped and REFUTED against real corpora, because parallel tool fan-out false-positives as abandoned branches on most sessions.",
"code": [
{
"path": "src/show/branch.rs",
"lines": "3-7",
"snippet": "//! A Claude Code rewind, retry, or parallel lane leaves one plain DAG fact: some record\n//! has MORE THAN ONE conversation child (a later `parentUuid` re-attach). Which side is\n//! \"live\" is NOT computable from the jsonl: a live/abandoned classifier was prototyped\n//! and refuted against real corpora (parallel tool fan-out makes sibling leaves that\n//! false-positive as abandoned branches on most sessions). So csift reports the facts"
}
],
"instrument": "`csift show @<session> --branch-points --format json`. Counting rule: records with 2+ conversation children, where a conversation child is a user/assistant record that is not a tool_result carrier, not isMeta and not a compaction summary.",
"located": {
"claude_code": null,
"csift": null,
"source": "src/show/branch.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift show @<id> --branch-points and csift show @<id> --branch-points --format json | jq -c '{kind}' | sort | uniq -c ; an independent python recount over the same jsonl implementing the stated rule, and a second recount with tool_result carriers ADMITTED as children",
"observed": "csift header: '13596 conversation record(s) - 220 branch point(s)'; JSON stream = 1 header + 220 kind:\"branch-point\" rows + 1 summary. Independent python recount over the same 91975-line transcript: conversation records 13596, branch points 220 - exact match on both numbers. Same recount with tool_result carriers admitted as conversation children: 434 branch points (1.97x), confirming that parallel tool fan-out shares a parent by construction and would false-positive as forks. Top-ranked fork: 4 children, widest inter-child gap 13h35m58s. No live/abandoned field appears in the text output or in any JSON row.",
"rule": "A conversation child is a user/assistant record that is not a tool_result carrier, not isMeta and not a compaction summary; a branch point is a parentUuid with 2 or more such children. Both numbers are reproducible from the raw jsonl without csift.",
"note": "This is the strongest form available: an outside recount of the same file reproduces csift's two headline numbers exactly, and the naive variant reproduces the false-positive the claim says refuted a classifier. src/show/branch.rs:3-7 is verbatim."
}
]
},
{
"id": "MISC-007",
"area": "misc",
"behavior": "All five doc strings and the display-only virtual concept reproduce verbatim in 2.1.258. Two wording fixes: the binary spells the field snake_case as `is_virtual`, so the claim's `\"isVirtual\":` probe alone cannot decide the absence - both spellings must be checked, and both measure 0. And the 'measured over 24 non-record types' figure is a corpus-wide tally, not a per-file one: a single 91975-line transcript yields 22 line types, 20 of them non-record, all at zero message{}.",
"depends": "csift uses these to justify marking the promoted non-record leaves `llm_visible = false` with the careful wording 'not in the surviving conversation' rather than the stronger, false claim that the model never saw them - the load-bearing instrument being that ZERO of them carries a `message{}` field.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "172-178",
"snippet": " /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n /// over every non-record line type in the corpus; every user/assistant record\n /// does), and Claude Code's\n /// own source labels them REPL-render internals. DAG threading is NOT the\n /// instrument here - later user records name a `turn_duration` uuid as parentUuid\n /// (chain continuity), and `preservedMessages` lists these uuids at the same rate"
}
],
"instrument": "`strings -n 6` over the installed Claude Code binary filtered for `turn_duration|away_summary|scheduled_task_fire|isVirtual`, reading the surrounding doc strings (counting rule: one per extracted string); then `rg -c '\"isVirtual\":' ~/.claude/projects -g '*.jsonl'` must be 0 (counting rule: one per matching line).",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc_strings.txt; then rg -No '@internal[^\\n]{0,220}(turn_duration|away_summary|scheduled_task_fire|agents_killed|api_error)[^\\n]{0,60}' cc_strings.txt and rg -No 'is_virtual:x\\(!0\\)\\.optional\\(\\)\\.describe\\(\"@internal[^\"]{0,200}' cc_strings.txt ; per-project-dir rg -cNI '\"isVirtual\"' and '\"is_virtual\"' over the 5 largest project dirs ; a python census over one 91975-line transcript tallying, per top-level type (system split by subtype), how many records carry a message{} object",
"observed": "Binary doc strings, verbatim: turn_duration - '@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the \\'Done in Ns\\' / \\'Waiting for N agents\\' line. From internal SystemMessage \\'turn_duration\\'.'; away_summary - '@internal Summary of what happened while the user was away (background tasks completed, notifications accumulated). From internal SystemMessage \\'away_summary\\'.' plus, separately, 'the session recap (shown when you return after being away for 5+ minutes) is disabled'; scheduled_task_fire - '@internal Emitted when a scheduled task (cron) fires. content is the render text. From internal SystemMessage \\'scheduled_task_fire\\'.'; agents_killed - '@internal Emitted when background agents are terminated (e.g. on interrupt). REPL renders an \\'agents killed\\' banner. From internal SystemMessage \\'agents_killed\\'.'; api_error - '@internal Retryable-API-error frame carrying the plain-data error snapshot and retry counters. REPL renders the retry banner from this. Wire twin is SDKAPIRetryMessage (\\'api_retry\\').'; virtual - 'is_virtual:x(!0).optional().describe(\"@internal Display-only: rendered in the UI but filtered before API send.' ON DISK: 0 occurrences of BOTH \"isVirtual\" and \"is_virtual\" across the 5 largest project dirs (7612 jsonl files). message{} census, one transcript, 22 distinct line types: assistant 12149/12149 and user 7494/7494 carry message{}; the other 20 types carry it 0 times - attachment 49902, last-prompt 3701, permission-mode 3698, mode 3697, ai-title 3697, agent-name 3624, file-history-snapshot 1180, system/stop_hook_summary 974, queue-operation 751, system/turn_duration 408, file-history-delta 248, system/away_summary 225, bridge-session 85, atis-latch 83, system/compact_boundary 33, system/model_refusal_fallback 8, cost-state 8, system/local_command 7, system/agents_killed 2, system/informational 1.",
"rule": "One per extracted binary string; one per matching LINE for the on-disk key probe; one per parsed record for the message{} census, keyed by top-level type with system split by subtype.",
"note": "The load-bearing instrument survives refutation cleanly: the message{} split is perfect (2 types at 100%, 20 types at 0%) in a single-file census a stranger can rerun, which is what licenses the careful 'not in the surviving conversation' wording. The absolute-path and away-threshold strings were read in full context to make sure neither contradicts the claim."
}
]
},
{
"id": "MISC-008",
"area": "misc",
"behavior": "The load-bearing assertion holds and is what matters: NEITHER phrase ever lands on a `system` record (0 of 62 and 0 of 68). The population numbers have grown since they were taken and should be dropped or re-dated: 'Cogitated for' is now 62 lines in 12 files (not 11 in 2), the background-agents phrase 68 lines in 17 files (not 20). The last-prompt bucket reproduces exactly at 3. Two carriers absent from the original bucket list now appear for both phrases: `attachment` and `queue-operation`.",
"depends": "csift must not source turn-timing or pending-agent facts from those phrases; the only structural carriers are `turn_duration.durationMs` and `turn_duration.pendingBackgroundAgentCount`, which its promoted-leaf render reads by key.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "197-204",
"snippet": " for (key, v) in [\n (\"messageCount\", rec.message_count.as_ref()),\n (\n \"pendingBackgroundAgentCount\",\n rec.pending_background_agent_count.as_ref(),\n ),\n (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),\n ] {"
}
],
"instrument": "`rg -cNI 'Cogitated for' ~/.claude/projects -g '*.jsonl'`, then re-parse each hit line and bucket by top-level `type`; expect zero on `system`. Counting rule: one per matching LINE.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each project dir: rg -lNI 'Cogitated for' <dir> -g '*.jsonl' (collecting the file list), then a python pass re-parsing every matching line and bucketing it by top-level `type` with system split by subtype; the same for the regex 'background agent\\(?s?\\)? to finish' ; rg -No '[^\\n]{0,50}Waiting for[^\\n]{0,90}' cc_strings.txt | rg -i agent ; rg -NI '\"turn_duration\"' <one transcript> | jq -s to tally durationMs / pendingBackgroundAgentCount presence",
"observed": "'Cogitated for': 62 matching lines across 12 files - assistant 32, user 25, attachment 4, queue-operation 1, system 0. Background-agents phrase: 68 matching lines across 17 files - user 29, assistant 28, attachment 5, queue-operation 3, last-prompt 3, system 0. Binary: the only agent-bearing 'Waiting for' render wording is inside the turn_duration doc string, verbatim \"REPL renders the 'Done in Ns' / 'Waiting for N agents' line\"; 'Cogitated'/'Cogitating' appear only inside the spinner-verb word list. Structural carriers, one transcript: 408 system/turn_duration records, durationMs present on 408/408, pendingBackgroundAgentCount present on 5/408 (emitted only when non-zero); csift renders both by key.",
"rule": "One per matching LINE, bucketed by the parsed top-level `type` (system records split by subtype). File counts are distinct files containing at least one matching line.",
"note": "The counts are inherently unstable - both phrases are ordinary English that any transcript discussing the harness will quote, which is exactly why they are unusable as a data source and why the structural carriers are. pendingBackgroundAgentCount being present on only 5 of 408 turn_duration records is worth recording: a reader must treat its absence as zero, not as missing data."
}
]
},
{
"id": "MISC-009",
"area": "misc",
"behavior": "Claude Code exports `CLAUDE_CODE_SESSION_ID` into every Bash tool environment and its value is exactly the calling session's jsonl basename - but it names the TOP-LEVEL session in EVERY lane, including inside an in-process subagent, whose own id is withheld from the Bash env and handed only to hooks.",
"depends": "`csift whoami` uses this variable and nothing else (never a process-tree walk, never most-recent-mtime, which is a false-positive trap under concurrent sessions), refuses to guess when it is absent, reports `is_subagent`/`parent_session_id`/`depth` as null in the env form, and prints a lane note on every `@main` resolution; a subagent must use `@trap:<marker>` to name itself.",
"code": [
{
"path": "src/whoami.rs",
"lines": "3-7",
"snippet": "//! ## Detection (verified empirically inside a live Claude Code Bash tool, 2026-06-07)\n//!\n//! Claude Code exports `CLAUDE_CODE_SESSION_ID` into its Bash tool environment.\n//! It was confirmed to equal exactly the session's own jsonl filename:\n//!"
},
{
"path": "src/whoami.rs",
"lines": "33",
"snippet": "const SESSION_ID_ENV: &str = \"CLAUDE_CODE_SESSION_ID\";"
}
],
"instrument": "In a Bash call run `echo $CLAUDE_CODE_SESSION_ID` and `ls ~/.claude/projects/*/$CLAUDE_CODE_SESSION_ID.jsonl` - expect exactly one file; `csift whoami` with the variable unset must error with guidance rather than pick a session. From a SUBAGENT's Bash call the same echo prints the parent uuid, while `csift whoami @trap:<a fresh marker>` prints the subagent's own ancestry chain - only a live subagent can show the divergence.",
"located": {
"claude_code": "2.1.258",
"csift": "0.1.0",
"source": "SPEC.md sections 6.3 and 6.3a; AGENTS.md section 3.8"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Run from inside a live SUBAGENT's Bash call: echo ${CLAUDE_CODE_SESSION_ID:+yes} ; ls ~/.claude/projects/*/$CLAUDE_CODE_SESSION_ID.jsonl | wc -l ; ls ~/.claude/projects/*/*/subagents/**/*$CLAUDE_CODE_SESSION_ID* | wc -l ; env | rg -c '<this subagent's own hex id>' ; csift whoami --format json ; csift whoami @trap:QuietAmberFalcon7412 ; env -u CLAUDE_CODE_SESSION_ID -u CODEX_COMPANION_SESSION_ID csift whoami",
"observed": "The variable was SET inside a subagent lane and its value resolved to exactly 1 top-level ~/.claude/projects/<dir>/<uuid>.jsonl and 0 files under any subagents/ directory - i.e. it named the TOP-LEVEL session, not the running lane. No environment variable in that same Bash environment contained the subagent's own id (grep count 0 over the full env). csift whoami --format json returned is_subagent null, parent_session_id null, depth null, and printed on stderr: 'csift: note: resolved via env ($CLAUDE_CODE_SESSION_ID), which names the TOP-LEVEL session in every lane - from a subagent this is the PARENT's id, not yours. Lane fields are null under env-only resolution'. The text form printed 'lane unknown from env alone (a subagent sees its parent's id here)'. csift whoami @trap:<a fresh 3-CamelCase-word + 4-digit marker> resolved on the FIRST try to the subagent's own transcript under <parent-uuid>/subagents/workflows/wf_*/agent-<hex>.jsonl and printed the ancestry chain 'subagent <hex> <- you (subagent, depth 0)' over 'session <parent uuid> ^ top-level root'. With both variables unset, whoami exits with 'cannot identify the calling session: CLAUDE_CODE_SESSION_ID is not set (old Claude Code build, or running outside Claude Code). Do NOT trust most-recent-mtime - many sessions may be live at once. Pass an explicit `@<uuid>` target'.",
"rule": "One Bash call executed in a subagent lane. The env value is resolved against the projects tree by basename, expecting exactly one top-level file and zero subagent files; the divergence is established by @trap resolving to a DIFFERENT transcript in the same call.",
"note": "This is the live-subagent instrument the claim names, and it ran: the divergence between the env value (top-level uuid) and the true lane (a subagent transcript) was observed in a single Bash invocation, and the subagent's own id is provably absent from its environment. Not instrumented: the 'handed only to hooks' half - no hook fired during this run, so that clause rests on the code path alone. src/whoami.rs:3-7 and :33 are verbatim."
}
]
},
{
"id": "MISC-010",
"area": "misc",
"behavior": "Claude Code's config home is `CLAUDE_CONFIG_DIR ?? <os home> + \"/.claude\"`, NFC-normalized: when the variable is set, every path that would be `~/.claude/...` lives under that directory instead. Because the coalescing operator only tests for null/undefined, an EMPTY-STRING `CLAUDE_CONFIG_DIR` is taken as set.",
"depends": "csift resolves its data root with the precedence `--claude-home` > `$CLAUDE_CONFIG_DIR` (non-empty) > the OS home's `.claude`; the empty-string case is a deliberate documented divergence, and any other drift makes every subcommand read a different corpus than the harness writes.",
"code": [
{
"path": "src/path/home.rs",
"lines": "60-63",
"snippet": "/// Claude Code's own config-dir relocation env var. When set, \"every `~/.claude` path\n/// lives under that directory instead\", so csift - which reads Claude Code's data - must\n/// honor it to keep pointing at the same files.\npub const CLAUDE_CONFIG_DIR_ENV: &str = \"CLAUDE_CONFIG_DIR\";"
},
{
"path": "src/path/home.rs",
"lines": "76-83",
"snippet": "pub(crate) fn resolve_claude_home(\n flag_override: Option<&Path>,\n config_dir_env: Option<&OsStr>,\n home: &Path,\n) -> PathBuf {\n if let Some(p) = flag_override {\n return p.to_path_buf();\n }"
}
],
"instrument": "Set `CLAUDE_CONFIG_DIR` to a fixture tree containing `projects/<encoded>/<uuid>.jsonl` and run `csift list`; expect only the fixture rows. The pure precedence function `resolve_claude_home(flag, env, home)` in `src/path/home.rs` is unit-tested without touching the process-global override. For the harness side, `strings` over the installed Claude Code binary filtered for `CLAUDE_CONFIG_DIR` shows one reader coalesced against a home-dir join followed by an NFC normalize. Counting rule: one config-home resolver per match.",
"located": {
"claude_code": "2.1.228",
"csift": "0.1.0",
"source": "AGENTS.md section 7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -a -o -N '.{200}CLAUDE_CONFIG_DIR.{260}' ~/.local/share/claude/versions/2.1.258 ; CLAUDE_CONFIG_DIR=<fixture tree> csift list --format json | jq -rc 'select(.kind==\"session\")|.session_id' ; CLAUDE_CONFIG_DIR=$HOME/.claude csift --claude-home <fixture tree> list --format json | jq -rc 'select(.kind==\"session\")|.session_id' ; CLAUDE_CONFIG_DIR=\"\" csift list --max-count 1 --format json | jq -rc 'select(.kind==\"header\")|.sessions_in_scope'",
"observed": "The resolver, read verbatim out of the 2.1.258 binary: `function s(){return process.env.CLAUDE_CONFIG_DIR}` followed by `var Se=Zo(()=>(s()??i(R(),\".claude\")).normalize(\"NFC\"),s);` - a nullish-coalescing `??` against a homedir join, then .normalize(\"NFC\"), exactly as the claim states. Because `??` tests only null/undefined, an empty string IS taken as set. A DIFFERENT, legacy path uses the OR form instead (`process.env.CLAUDE_CONFIG_DIR||z()`), and the string 'the configuration home (CLAUDE_CONFIG_DIR) is not an absolute path' was read in context: it is a bail reason inside one feature's placement check, not a gate on the resolver. csift side: with only the env set, list emitted exactly the 1 fixture session and nothing else; with --claude-home on the fixture and the env pointed at the real home, list again emitted exactly that 1 session (flag wins); with CLAUDE_CONFIG_DIR=\"\" csift fell back to the OS home and reported sessions_in_scope 7608 - the documented divergence from the harness's `??`.",
"rule": "One config-home resolver per binary match; one emitted session row per fixture transcript (the fixture holds exactly 1, so any leakage from the real home is immediately visible as extra rows).",
"note": "The resolver was found as a single expression rather than inferred, so the NFC normalize and the empty-string consequence are both direct reads rather than deductions. Worth noting for future drift: the binary now also carries an absolute-path validation message and a 'CLAUDE_CONFIG_DIR no longer names ...' mid-run-change warning, neither of which gates the resolver in 2.1.258 - if either is ever hoisted into `Se()`, the empty-string arm changes and this claim would need re-checking. src/path/home.rs:60-63 and :76-83 are verbatim."
}
]
},
{
"id": "MISC-011",
"area": "misc",
"behavior": "Add: the cutoff test is per FILE on stat().mtime; deleting a transcript also unlinks two siblings <uuid>.ccr-tip.json and <uuid>.precompact.json; a transcript past the cutoff can still be SPARED by a desktop retention exemption (counted as transcriptsExemptedDesktop, skipped under hipaa/zdr modes); the .jsonl unlinks are deferred to the end of the project directory and the whole batch is abandoned if the error counter rose during that directory; cleanupPeriodDays has a minimum of 1 and 0 is rejected outright.",
"depends": "csift can only read what survives retention, so its root help tells the operator to check and raise the setting; `--resolve-persisted`, `image --out`, `recover --list-backups` and every subagent-spanning surface read files under that sidecar tree, and any corpus-wide absence claim from `search` is bounded by whatever retention already removed.",
"code": [
{
"path": "src/cli/root.rs",
"lines": "154-156",
"snippet": " Claude Code deletes transcripts older than `cleanupPeriodDays` (default 30!).\\n \\\n Check `jq '.cleanupPeriodDays // 30' ~/.claude/settings.json` and consider raising\\n \\\n it; csift can only read what survives.\\n\\n\\"
},
{
"path": "src/model/record.rs",
"lines": "138-141",
"snippet": " /// (a coverage annotation). `backupFileName` is usually present (measured 83-98%\n /// across real corpora), but the store it names is PRUNED and its content has no\n /// transcript anchor, so it is never used to fabricate content; `recover\n /// --list-backups` lists the store itself. Additive + tolerant."
}
],
"instrument": "The stated expectation 'expect no orphaned <uuid>/ sidecar directories' is wrong: 5 of 47 uuid-shaped sidecar dirs on this machine have no sibling .jsonl. The sweep only rm -r's a sidecar when IT deletes the matching transcript; a transcript removed any other way leaves the sidecar behind, and the sweep's orphan branch then prunes only subagents/ workflows/ remote-agents/ contents past the cutoff and rmdir's what becomes empty.",
"located": {
"claude_code": "2.1.233",
"csift": "0.7.6",
"source": "SPEC.md section 6 v0.7.6 ledger; CHANGELOG 0.7.6; dev session 2026-08-16"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Number of days to retain chat transcripts|cleanupPeriodDays must be at least 1'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function ehr\\(\\).{0,4200}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function x\\(.{0,700}'; jq '.cleanupPeriodDays // 30' ~/.claude/settings.json; python3 census of uuid-shaped sidecar dirs under ~/.claude/projects (a uuid-shaped subdirectory of a project dir; orphan = no sibling <uuid>.jsonl)",
"observed": "Binary 2.1.258: \"Number of days to retain chat transcripts before automatic cleanup (default: 30). Minimum 1. Use a large value for long retention; use --no-session-persistence to disable transcript writes entirely.\" and \"cleanupPeriodDays must be at least 1. To keep transcripts for a long time, set a large number (e.g. 3650 for ~10 years).\" The per-file cutoff test is MTIME: `let p=await n.stat(e); if(!(p.mtime<r))return a.filesRetainedFresh++,!1; ... await n.unlink(e)`. The sidecar removal, guarded on a uuid-shaped stem: `if(k.name.endsWith(\".jsonl\")){r.transcripts++;let j=k.name.slice(0,-6);if(ge(j)){await a.unlink(d(R,`${j}.ccr-tip.json`)).catch(()=>{}),await a.unlink(d(R,`${j}.precompact.json`)).catch(()=>{}),C.push(re(T)),await a.rm(d(R,j),{recursive:!0,force:!0}).catch(()=>{r.errors++})` - a recursive rm of the whole sibling <uuid>/ tree. Sweep telemetry schema: tengu_retention_sweep {phase, skipped, skipReason, usedDefault, periodDays, transcriptsDeleted, transcriptsExemptedDesktop, sessionFilesDeleted, artifactsDeleted, filesRetainedFresh, filesPastCutoff, historyEntriesPruned, errors}. On disk: cleanupPeriodDays = 730 here, oldest surviving transcript mtime 2026-06-13 (81 days), newest 2026-09-02; 47 uuid-shaped sidecar dirs, of which 5 have NO sibling <uuid>.jsonl, each holding only tool-results/ with 1 file, newest child 2026-06-01..2026-06-28.",
"rule": "One session per <uuid>.jsonl; one sidecar per uuid-shaped sibling directory; orphan = a uuid-shaped directory with no <uuid>.jsonl beside it. Retention age = now minus the file's mtime (stat().mtime, so reads never renew it).",
"note": "Retention deletion is UNEXERCISED on this machine (cleanupPeriodDays = 730, oldest file 81 days old), so the deletion path is read out of the 2.1.258 binary rather than observed running. The default-30 half is a binary string, the mtime half and the recursive sidecar rm are binary code, and the orphan census is on-disk."
}
]
},
{
"id": "MISC-012",
"area": "misc",
"behavior": "The cap is counted in JavaScript String.length, i.e. UTF-16 code units, not Unicode scalars. Identical for BMP text (all CJK the claim cares about), but an astral character (emoji, CJK ext-B) counts 2. The 'per hook' framing is right for SessionStart/Setup; on PreToolUse/UserPromptSubmit/PostToolUse/Stop/SubagentStop/SubagentStart/PostToolBatch/PostToolUseFailure/UserPromptExpansion an earlier sanitizer truncates additionalContext at 8000 characters or 200 lines, so those events never reach the 10000 threshold.",
"depends": "`csift verbatim --slice N --window N` packs the reconstruction into character-counted chunks (the unit the cap counts, so a CJK-heavy document is not 3x over-counted the way a byte budget would be) and concatenating the slices reproduces the text exactly, so one SessionStart hook per slice keeps every injected chunk under the wall.",
"code": [
{
"path": "src/cli/verbatim_whoami_args.rs",
"lines": "285-288",
"snippet": " /// fanning a >10K reconstruction across several SessionStart hooks: Claude Code caps EACH\n /// hook's `additionalContext` at 10,000 CHARACTERS (over-cap is replaced by a file-path +\n /// short preview, i.e. the body is effectively LOST to the model), so one hook per slice\n /// keeps every injected chunk under the wall. Slicing is DETERMINISTIC (same session +"
},
{
"path": "src/turns/render.rs",
"lines": "290-292",
"snippet": "/// Greedily pack a document's LINES into chunks of at most `window` CHARACTERS (Unicode\n/// scalars - the unit Claude Code's 10,000-char additionalContext cap counts, so a CJK-heavy\n/// document is NOT 3× over-counted the way a byte budget would). A line longer than the"
}
],
"instrument": "Install a SessionStart hook emitting more than 10,000 characters of additionalContext, then read the resulting `hook_additional_context` attachment with `csift search '' @<id> --additional-context` - it holds a path and a preview, not the body; inject a CJK block of 9,999 scalars to show the unit is scalars, not bytes. Counting rule: Unicode scalars in the injected string versus the cap. Only a live session with a hook can show it.",
"located": {
"claude_code": "2.1.258",
"csift": "0.3.0",
"source": "SPEC.md section 6.8; src/turns/render.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function ipe\\(.{0,900}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'NQn=1e4' with 250 bytes of surrounding context; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Ujr=\\{reason.{0,300}'; python3 census counting Unicode scalars in every ~/.claude/projects/*/*/tool-results/hook-*.txt",
"observed": "Constant: `var aJ=50000,WNe=500000;var QSt=4,LQn=400000,MQn=200000,$w=50,NQn=1e4,$7e=1e4,FQn=1e5;` - NQn = 1e4 = 10000. Use: `async function ipe(e,n,r,{threshold:o=NQn,storageV5:d}={}){if(e.length<=o)return e;let f=await $3(e,`hook-${n}-${r}`,kS(),d); ...}` reached from `if(Rn.additionalContext) ... yield{additionalContexts:[await ipe(Rn.additionalContext,`${o}-${Bn}`,\"additionalContext\",{storageV5:we})]}`. The over-cap replacement is built by a helper emitting `Output too large (${size}). Full output saved to: ${n.filepath}` then `Preview (first ${...}):` then the preview - a path plus a short preview, exactly as claimed; if the persist itself fails the fallback is `${e.slice(0,o)}` + `[Hook ${r} truncated at ${o} chars - persist-to-disk failed: ...]`. On disk: 166 persisted hook-output files, MINIMUM length 10029 Unicode scalars, maximum 54900 - nothing at or below 10000 was ever persisted. A separate, smaller sanitizer exists for the events routed through hookSpecificOutput validation: `var Ujr={reason:2000,stopReason:2000,systemMessage:4000,additionalContext:8000,permissionDecisionReason:2000},u4t=200` - 8000 chars / 200 lines; its event switch covers PreToolUse, UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, SubagentStart, PermissionDenied - SessionStart and Setup are NOT in it, so for the SessionStart recipe the 10000-char persist threshold is the operative wall.",
"rule": "Count Unicode scalars of every file matching ~/.claude/projects/*/*/tool-results/hook-*.txt; the MINIMUM bounds the persist threshold from above (10029 > 10000). The constant and its comparison bound it from below.",
"note": "csift's own doc comments (src/cli/verbatim_whoami_args.rs:285-288 and src/turns/render.rs:290-292) say 'CHARACTERS (Unicode scalars)'. Correct for BMP text; the exact unit is UTF-16 code units. Both comments exist verbatim in the current files."
}
]
},
{
"id": "MISC-013",
"area": "misc",
"behavior": "Claude Code runs same-event hooks CONCURRENTLY and concatenates their outputs in COMPLETION order, so N hooks registered on one event inject in a nondeterministic sequence.",
"depends": "The documented SessionStart(compact) recipe uses a PPID-namespaced done-flag barrier to force slice order and a 9,000-character window to stay under the injection cap; csift ignores the resulting attachment record by default, so re-firing the hook creates no feedback loop.",
"code": [
{
"path": "src/turns/render.rs",
"lines": "34",
"snippet": " // inject it under the 10,000-char additionalContext cap. Two sub-modes:"
}
],
"instrument": "Register the slice hook script N times, trigger a compaction, then `csift search '<the slice header>' @<uuid> --additional-context --count-by label` - N hits under `harness.meta.hook`, in slice order only when the barrier is present. Counting rule: one attachment record per registered hook.",
"located": {
"claude_code": null,
"csift": "0.3.0",
"source": "SKILL.md Hook 1 (race fix and the injection cap)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 over the 5 transcripts that carry a multi-hook SessionStart injection: collect every attachment record with type=='hook_success' and hookEvent=='SessionStart' whose stdout carries the marker, in file-line order, group into bursts (consecutive records within 8 lines) and compare emission orders. Plus jq '.hooks | to_entries[] | ...' ~/.claude/settings.json for the declared hook counts.",
"observed": "Declared config: SessionStart has 34 hooks over 5 matcher groups (one group of 30 identically-shaped hooks in a fixed declared order). 16 marker-bearing hook_success records, 5 bursts with >=2 records. Emission orders: ('HEADER','file','file') x3, ('HEADER','file','file','file') x1, ('file','file','HEADER') x1 - the header-emitting hook came first in 4 of 5 bursts and LAST in 1, from an unchanged declared order. The hook_additional_context attachment's `content` is an ARRAY whose entry order matches the burst order (a 3-entry record with the header at index 2 was observed alongside three with it at index 0). Binary 2.1.258 builds one async generator per hook - `let yn,nt=Ge.map(async function*({hook:bn,pluginRoot:Rn,pluginId:Dn,...},Rr){...})` - and drains them all through a single merge combinator, `for await(let bn of AOe(nt))`, accumulating each yielded additionalContext into one list.",
"rule": "One burst = the run of consecutive hook_success attachment records for hookEvent SessionStart carrying the marker, within 8 lines of one another; the burst's emission order is the file-line order of those records. Nondeterminism = two or more distinct orders across bursts under one unchanged declared order.",
"note": "n = 5 bursts, so the observable (a nondeterministic sequence under a fixed declaration) is established but thinly sampled. The MECHANISM ('concurrently', 'completion order') is inferred from the per-hook async generators plus the merge drain; the combinator itself could not be resolved in the minified bundle. Do NOT cite the binary's `--hook-concurrency <n>` flag as evidence - it belongs to the self-hosted spawn-runner orchestrator, not to session lifecycle hooks."
}
]
},
{
"id": "MISC-014",
"area": "misc",
"behavior": "Claude Code's own wire format for transcript lines is COMPACT JSON with no whitespace around the `\"role\":` colon, but a hand-authored or reserialized line (a default JSON dump) is valid JSON carrying `\"role\": \"user\"` and is the SAME record.",
"depends": "csift's stage-1 candidate keeps use the serialization-tolerant matchers `line_has_role_marker` / `line_has_user_role_marker`; the old exact compact byte needle silently DROPPED a reserialized line one layer BEFORE any malformed counter could see it - not skipped, not counted, simply invisible on every surface: no preview, no count, no match, zero disclosure.",
"code": [
{
"path": "src/parse/lines.rs",
"lines": "42-47",
"snippet": "/// Serialization-tolerant role-marker test - THE stage-1 candidate needle for\n/// message records (R13). CC's own wire format is compact JSON, but a hand-authored\n/// or reserialized line may carry whitespace around the colon (`\"role\": \"user\"`) -\n/// valid JSON, the same record. The old exact-byte needles (`\"role\":\"user\"`)\n/// silently DROPPED such lines one layer BEFORE any malformed counter could see\n/// them: not skipped, not counted, simply invisible on every surface. One `memmem`"
},
{
"path": "src/parse/lines.rs",
"lines": "87-98",
"snippet": "/// `\"role\"` is `\"user\"` OR `\"assistant\"` (any JSON whitespace around the colon) -\n/// the candidate test for `search`/`show`/`verbatim`/`list`/`stats` stage-1 filters.\npub fn line_has_role_marker(line: &[u8]) -> bool {\n line_role_value_matches(line, true, true)\n}\n\n/// `\"role\"` is `\"user\"` only - the genuine-user/carrier hook `files`/`recover` use\n/// (their assistant-side coverage rides tool-name needles, so admitting every\n/// assistant text record here would repeal their §7 prefilter).\npub fn line_has_user_role_marker(line: &[u8]) -> bool {\n line_role_value_matches(line, true, false)\n}"
}
],
"instrument": "Write a fixture transcript with a default JSON dump (which emits `\"role\": \"user\"`) under a `--claude-home` tree and confirm `csift search` finds its content. Counting rule: matched records for one known needle with and without the space, expected 1 in both.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 7d; src/parse/lines.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "On one transcript: rg -c '\"role\":\"user\"' FILE; rg -c '\"role\": \"user\"' FILE (and the assistant pair). Then a fixture under --claude-home: write one record with python json.dumps default separators (emits '\"role\": \"user\"'), run `csift --claude-home <tmp>/.claude search 'zebrafish marker' -c`; rewrite the same record with separators=(',',':') and re-run.",
"observed": "One 48460-line transcript: 3694 lines carry `\"role\":\"user\"` and 0 carry `\"role\": \"user\"`; 7223 carry `\"role\":\"assistant\"` and 0 carry `\"role\": \"assistant\"` - the wire format is compact, with no whitespace around the colon. Fixture: the reserialized (spaced) line returns 1; the same record recompacted returns 1.",
"rule": "Matched records for one known needle, with and without the space around the role colon; expected 1 in both.",
"note": "Both csift code sites are present verbatim: src/parse/lines.rs:42-49 (the R13 doc comment) and src/parse/lines.rs:87-98 (line_has_role_marker / line_has_user_role_marker)."
}
]
},
{
"id": "MISC-015",
"area": "misc",
"behavior": "Because Claude Code JSON-encodes string content in the raw line (`\"` becomes `\\\"`, every control character below 0x20 and DEL becomes `\\uXXXX` / `\\n`), a rendered text can differ byte-for-byte from the raw line wherever whitespace or an escapable character sits.",
"depends": "csift derives search prefilter needles NECESSITY-ONLY and applies a per-needle safety predicate (whitespace-free, JSON-escape-free, at least 3 bytes), plus a marker list for the small closed set of render paths that fabricate text not present verbatim in the raw line; a needle failing the predicate would silently drop real matches, which is the one place the no-silent-truncation contract actually bites.",
"code": [
{
"path": "src/search/matcher.rs",
"lines": "493",
"snippet": "// The agents-stopped kill notice renders a fabricated `[subagent stopped]` head."
}
],
"instrument": "`csift search 'hello world' @<a session containing that phrase across a newline>` must still match; the prefilter unit tests in `src/search/tests/matcher.rs` pin the predicate. Counting rule: matched records with and without the whitespace in the pattern.",
"located": {
"claude_code": null,
"csift": "0.9.4",
"source": "SPEC.md section 7d"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Fixture under --claude-home with two assistant records, text 'alpha\\nbravo' (a real newline) and 'he said \"charlie delta\" loud'; then `csift --claude-home <tmp>/.claude search 'alpha\\s+bravo' -c`, `... search 'said \"charlie' -c`, `... search 'charlie' -c`. Plus rg -c on one real transcript for JSON-escaped newlines and escaped quotes.",
"observed": "The raw line is `...\"type\":\"text\",\"text\":\"alpha\\nbravo\"}]}}` - the rendered text differs byte-for-byte from the raw line at the whitespace. All three searches return 1: a pattern whose match spans the escaped newline (1), a pattern containing a character that is escaped in the raw line (1), and the plain-literal control (1). On one real 48460-line transcript, 2216 lines carry a JSON-escaped newline and 4765 carry an escaped quote.",
"rule": "Matched records for a needle whose rendered form contains whitespace or a JSON-escapable character, versus the plain-literal control; expected 1 for each.",
"note": "The claim's own instrument sentence points at src/search/tests/matcher.rs for the predicate unit tests; the fixture run above is the black-box equivalent and is what was executed. code-site note: src/search/matcher.rs: the `let mut verifiable: Vec<&[u8]> = vec![...]` block with the five markers (<task-notification>, \"answers\", User has answered your questions, Your questions have been answered, stopped by the user) is at lines 487-494, not 485-492; the text is otherwise verbatim."
}
]
},
{
"id": "MISC-016",
"area": "misc",
"behavior": "Corpus geometry at 2026-09-02: 7789 jsonl files (was 2437); largest file 687.5 MB across 91975 lines (was 225 MB / 115879); ladder min 82 B, p50 805 B, p90 3.5 KB, p99 50.0 KB, max 18.0 MB for a single line. The min (82 B) and the p50 order of magnitude are unchanged; the tail is what moved - a single line is now up to ~18 MB, 45x the claimed 400 KB maximum.",
"depends": "The tail reader does NOT step 64 KiB to 256 KiB. TAIL_CHUNK = 64 * 1024 (src/parse/lines.rs:8) and RevLines::fill DOUBLES the window without a ceiling until it contains a newline (`take = take.saturating_mul(2)`, src/parse/readers.rs:63-66) - which is exactly what makes an 18 MB line readable. The in-code comment on that loop still says 'guards the 400 KB single-line max' and is itself stale.",
"code": [
{
"path": "src/parse/lines.rs",
"lines": "30-38",
"snippet": " // SAFETY: read-only mmap of a file we just opened. The documented hazard is a\n // concurrent truncation by another writer; we never write through the map and\n // treat its length as fixed-at-open, which is the SPEC's accepted contract\n // (§7a). The crate lints `unsafe_code = \"deny\"` (unsafe forbidden crate-wide);\n // mmap is irreducibly unsafe and SPEC-mandated, so this is the single audited\n // call site that explicitly allows it.\n #[allow(unsafe_code)]\n let mmap =\n unsafe { Mmap::map(&file) }.with_context(|| format!(\"cannot mmap {}\", path.display()))?;"
}
],
"instrument": "`find ~/.claude/projects -name '*.jsonl' | wc -l` for the file count, `wc -lc` on the largest for lines and bytes, `awk '{print length}' <largest> | sort -n` for the percentile ladder. Counting rule: bytes per line excluding the trailing newline; percentiles over all lines of one file.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "find ~/.claude/projects -name '*.jsonl' -type f | wc -l; find ~/.claude/projects -name '*.jsonl' -type f -exec stat -f '%z %N' {} ';' | sort -rn | head -3; wc -lc < LARGEST; python3 ladder over LARGEST reading len(line.rstrip(b'\\n')) per line, sorting, and indexing int(q*n)",
"observed": "7789 jsonl files under ~/.claude/projects. Largest: 687.5 MB, 91975 lines. Ladder for that file: min 82 B, p50 805 B, p90 3536 B (3.5 KB), p99 51250 B (50.0 KB), max 18031027 B (17.2 MB). The previously-cited landmark file now measures 396.7 MB / 123044 lines with min 82 B, p50 774 B, p90 4535 B, p99 34126 B, max 13333694 B (12.7 MB).",
"rule": "File count = every *.jsonl under ~/.claude/projects (subagent transcripts and workflow journals included). Line length = bytes excluding the trailing newline; percentiles over all lines of ONE file, index = int(q*n) into the sorted list.",
"note": "The parse-timing figures in the claim (1.39 s parse-every-line vs 0.65 s prefilter-then-parse over a 0.28 s memchr floor) were NOT re-measured here; they were attached to a file that has since grown, so they should be re-benchmarked before being restated."
}
]
},
{
"id": "MISC-017",
"area": "misc",
"behavior": "The shrink is a RENAME over the path (compact temp -> transcript), not an in-place truncate; a reader that already mmapped the old inode keeps reading it intact, which is why the fixed-at-open contract survives compaction.",
"depends": "csift skips a BLANK final fragment uncounted (line_payload rejects an all-whitespace remainder, skipped_lines stays 0), but it does NOT skip a torn one: a `{`-framed partial record is counted as ONE malformed line (skipped_lines 1) - disclosed rather than hidden, and stable across repeated runs at exit 0. That is the correct behaviour under the no-silent-truncation contract, but it is counting, not skipping.",
"code": [
{
"path": "src/parse/lines.rs",
"lines": "101-109",
"snippet": "pub(crate) fn line_payload(line: &[u8]) -> Option<&[u8]> {\n let line = line.strip_suffix(b\"\\n\").unwrap_or(line);\n let line = line.strip_suffix(b\"\\r\").unwrap_or(line);\n if line.iter().all(u8::is_ascii_whitespace) {\n None\n } else {\n Some(line)\n }\n}"
}
],
"instrument": "Run `csift search '' @main` repeatedly against the currently-writing session while it works; every run must exit 0 with a stable `skipped_lines` count. Counting rule: `skipped_lines` from the JSON summary across repeated runs.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 7a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Three live probes plus one fixture. (a) python3: sample every main transcript touched in the last hour every 50 ms for 280 s, read only the appended bytes, count reads whose tail lacks a newline. (b) python3: sample ONE live subagent transcript's size and final byte every 1 ms for 45 s. (c) python3: sample the newest-mtime transcript every 100 ms for 40 s. (d) fixture under --claude-home: append a half-written record with no trailing newline, then run `csift --claude-home <tmp>/.claude search 'torncheck' --format json` three times; repeat with a blank (whitespace-only) tail. Plus `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'performCompactTranscript\\(.{0,2400}'`.",
"observed": "(a) 51 records appended across 9 live main transcripts in 280 s; torn-tail reads 0. (b) 31824 samples: grew 0, shrank 0, torn 0. (c) 400 samples: torn 0. So growth is routine and a torn final fragment was NOT observed in ~38000 samples. Shrink, from the binary: `async performCompactTranscript(e,n,r,o){... let f=`${e}.compact.tmp.${Its(4).toString(\"hex\")}` ... await Ti(f,e) ... s(\"tengu_transcript_compact\",{bytesBefore:C.size,bytesAfter:ke})}` - the compacted temp is RENAMED over the live transcript path, so the file at the path shrinks. (d) csift with a torn `{`-framed tail: matched=1 skipped_lines=1, exit 0, identical on 3 consecutive runs. With a blank tail: matched=1 skipped_lines=0.",
"rule": "Sample the file's size and final byte; torn tail = final byte != '\\n'. For csift, read `matched` and `skipped_lines` from the JSON summary's last line across repeated runs and compare.",
"note": "A torn final fragment was never caught in ~38000 samples across three probes (down to a 1 ms sampler), so on this machine the appends look atomic at the available resolution. The claim's torn-tail hazard is therefore supported only by csift's tested handling of a synthetic torn tail, not by an observed one. A writer-side probe (a hook or an fs-event watcher sampling inside the write) would decide whether CC ever leaves a partial line visible."
}
]
},
{
"id": "MISC-018",
"area": "misc",
"behavior": "The measured band for the record @trap needs - an assistant record carrying a tool_use - is 0.11 s to 3.11 s (n=6), not '~1 to 3.4 s'. The upper bound is right; the lower bound is much looser, so a main-lane FIRST try can occasionally win the race rather than 'normally missing'. Text-only assistant records lag further still (2.32-6.40 s). The contrast that proves the asynchrony is structural: user, attachment and queue-operation records land within 0.01-0.19 s of their own timestamp while assistant records take seconds.",
"depends": "csift's `@trap:<marker>` resolves first-try from a subagent but normally loses the race from the main thread, so the no-match error routes `@main` first and a re-run of the SAME marker second; a hook asking for the PREVIOUS prompt must exclude hits younger than now-3s and take the newest survivor.",
"code": [
{
"path": "src/path/trap.rs",
"lines": "14-19",
"snippet": "/// very command that launched this run. Mechanism + TIMING (subagent verified live 2026-07-12;\n/// main-lane mechanism re-measured 2026-08-29): a SUBAGENT's transcript flushes per content\n/// block as it closes, so its launching tool_use is on disk at dispatch and a first try\n/// resolves. The MAIN conversation writes an async flush of the COMPLETED assistant message\n/// that lands ~1-3.4s AFTER the tool was dispatched - a RACE, not a wait (a 263s command was\n/// observed with its unpaired tool_use already on disk 39s in). csift finishes well inside"
}
],
"instrument": "From a live main session run `csift whoami @trap:<a fresh 3-word-plus-4-digit marker>` - it misses; a second, SEPARATE Bash invocation with the same marker resolves. Counting rule: two invocations, two outcomes. Only a live session can time the flush.",
"located": {
"claude_code": "2.1.258",
"csift": "0.8.2",
"source": "SPEC.md section 6.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "`csift whoami @trap:<a fresh 3-CamelCase-word + 4-digit marker>` run ONCE from inside a live subagent's Bash call. Then python3: sample every main transcript touched in the last hour every 50 ms for 280 s, parse each appended line, and record lag = sample wall clock minus the record's own ISO `timestamp`, bucketed by record type (assistant records carrying a tool_use split out).",
"observed": "The trap resolved on the FIRST invocation, printing `subagent <id> <- you (subagent, depth 0)` with the session root and the transcript path - the launching tool_use was already on disk at dispatch. Flush lag over 280 s across 9 live main transcripts: assistant records CARRYING a tool_use n=6 min 0.11 s p50 0.65 s p90/max 3.11 s; assistant text-only records n=7 min 2.32 s p50 3.67 s max 6.40 s; user records (tool_result carriers) n=7 min 0.05 s max 0.19 s; attachment records n=27 min 0.02 s max 0.19 s; queue-operation n=4 max 0.01 s.",
"rule": "One trap invocation, one outcome. For the lag: sample each live main transcript's size at 50 ms, read only appended bytes, lag = sample time minus the record's own ISO timestamp; bucket by record type. The 0.01 s floor on queue-operation records bounds clock skew and sampler granularity.",
"note": "The main-lane half of the claim (a first `@trap` try from the top-level thread misses, a second resolves) could NOT be executed - this lane is a subagent, and the marker must be embedded in the very command that launches the run. What would decide it: a top-level session running `csift whoami @trap:<fresh marker>` in one Bash call and the SAME marker again in a second, separate Bash call, recording both outcomes. The evidence here is the flush-lag distribution that governs that race, plus the first-try subagent resolution."
}
]
},
{
"id": "MISC-019",
"area": "misc",
"behavior": "A background task can be designed never to return (a dev server, a file watcher), so there is no bound after which an open background task must have closed.",
"depends": "csift makes `--timeout` REQUIRED on `wait` and exits 124 on the bound - the GNU timeout convention and the single documented exception to the crate's 0-vs-non-zero exit law - rather than blocking forever on `--until stop`.",
"code": [
{
"path": "src/live/wait.rs",
"lines": "10-12",
"snippet": "/// The GNU `timeout` convention - the ONE documented exception to the crate's\n/// 0-vs-non-zero exit law (a monitor's timeout is a normal outcome scripts branch on).\nconst TIMEOUT_EXIT: u8 = 124;"
},
{
"path": "src/live/wait.rs",
"lines": "36-42",
"snippet": " // The timeout is REQUIRED (v0.10.0): a background task may never return by design\n // (a dev server, a watcher), so an unbounded wait on `stop` is a bug, not a wait.\n let Some(timeout_secs) = args.timeout else {\n bail!(\n \"wait needs --timeout <SECS>: a background task can be designed never to return \\\n (a dev server, a watcher), so a wait without a bound never ends. Pick a bound, \\\n branch on exit 124, and read the at-exit report; narrow what counts with \\"
}
],
"instrument": "Of the two e2e tests named, `p6_history_never_fires_and_timeout_is_124` is at tests/cli/live/wait.rs:45 but `wait_demands_a_timeout_and_reports_what_it_saw` lives at tests/cli/live/background.rs:198, not in wait.rs.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "CHANGELOG 0.10.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "`csift wait @<uuid> --until stop` (no --timeout), then `csift wait @<uuid> --until stop --timeout 2` against a live session; echo $? after each. Plus `csift status`-style background rows in the timeout report, and grep for the four named e2e tests under tests/.",
"observed": "Without --timeout: exit 1 with `csift: error: wait needs --timeout <SECS>: a background task can be designed never to return (a dev server, a watcher), so a wait without a bound never ends. Pick a bound, branch on exit 124, and read the at-exit report; ...`. With --timeout 2: exit 124, stdout first line `fired timeout`, stderr readiness line `csift: watching 114 file(s) from byte offsets; conditions: stop; timeout 2s`. The at-exit report listed two still-open background rows on that session - an async agent launched 2h 15m earlier and a shell launched 2h 16m earlier with 'output 0 B, last write 2h 16m ago' - i.e. background work that had not returned after two hours, which is the premise the required timeout exists for.",
"rule": "Two invocations, two exit codes: reject (non-zero, message names the reason) versus timeout (exactly 124).",
"note": "The Claude Code half of the claim - that a background task can be designed never to return - is a design statement rather than a falsifiable behaviour; the closest instrument reading is the pair of background rows still open after 2h+ in the at-exit report above. code-site note: `const TIMEOUT_EXIT: u8 = 124;` is at src/live/wait.rs:12 (its two-line doc comment at :10-11), and the exit is taken at src/live/wait.rs:215 via `std::process::exit(i32::from(TIMEOUT_EXIT))`. The bail block is at src/live/wait.rs:36-42, verbatim as quoted."
}
]
},
{
"id": "MISC-020",
"area": "misc",
"behavior": "Transcript growth is the only event stream a watcher gets, and content appended before the watch started is history the harness will never re-emit.",
"depends": "csift seeds a byte-offset baseline per watched file at start and prints a readiness line on stderr so a scripted caller can order its own trigger AFTER the snapshot; without that line an append racing the snapshot is correctly treated as history and the wait hangs to its timeout.",
"code": [
{
"path": "src/live/wait.rs",
"lines": "48-56",
"snippet": " // ── Baselines: snapshot every currently-watched file's length. Only bytes appended\n // AFTER these offsets are events; history is `search`'s job. ──\n let mut cursors: Vec<Cursor> = Vec::new();\n let seed = |path: PathBuf, is_main: bool, cursors: &mut Vec<Cursor>| {\n if cursors.iter().any(|c| c.path == path) {\n return;\n }\n let offset = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);\n let lane = crate::subagent::session_id_from_path(&path);"
},
{
"path": "src/live/wait.rs",
"lines": "80-83",
"snippet": " eprintln!(\n \"csift: watching {} file(s) from byte offsets; conditions: {}; timeout {timeout_secs}s{}\",\n cursors.len(),\n args.until.join(\" | \"),"
}
],
"instrument": "Append a matching record to a transcript BEFORE launching `csift wait` on it: the wait must not fire and must exit 124. Counting rule: one pre-append, one wait. The e2e tests `p5_wait_fires_only_on_post_start_events` and `p11_history_in_every_lane_never_fires` in tests/cli/live/wait.rs pin the baseline semantics in every lane.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "src/live/wait.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Fixture under --claude-home: write a transcript, APPEND a tool_use record for Grep BEFORE starting the wait, then `csift --claude-home <tmp>/.claude wait @<uuid> --until tool:Grep --timeout 3`; echo $?. Then, on the same file, start `... --until tool:Grep --timeout 12` while a background subshell appends an equivalent record 2 s after launch; echo $?.",
"observed": "Pre-append (the condition already satisfied by history): exit 124, stdout `fired timeout`. Post-baseline append: exit 0, stdout `fired tool:Grep` and `verdict running`. Both runs printed the readiness line on stderr before polling: `csift: watching 1 file(s) from byte offsets; conditions: tool:Grep; timeout <N>s`.",
"rule": "One pre-append then one wait, one exit code (expect 124); then one post-baseline append then one wait, one exit code (expect 0 naming the condition). The pair isolates the baseline: identical file, identical condition, only the append's timing relative to the snapshot differs.",
"note": "Both cited code sites are verbatim in the current file: the baseline snapshot block at src/live/wait.rs:45-53 and the readiness eprintln at src/live/wait.rs:77-80. Both named e2e tests exist (tests/cli/live/wait.rs:7 and :206)."
}
]
},
{
"id": "MISC-021",
"area": "misc",
"behavior": "A subagent lane can be spawned at any moment during a turn, so the set of transcripts belonging to one session is not fixed at the start of an observation.",
"depends": "csift's `wait` re-discovers child transcripts on every poll and seeds a newly appeared lane at baseline offset 0, since a file born after start is entirely post-start; discovering only at start would miss every event in a lane spawned mid-wait.",
"code": [
{
"path": "src/live/wait.rs",
"lines": "98-112",
"snippet": " // ── New-child discovery: a lane spawned after start joins the watch set with\n // baseline 0 (its whole content is post-start). ──\n if args.want_subagents() && !is_subagent_target {\n for sub in crate::subagent::subagent_transcript_files(&main).unwrap_or_default() {\n if !cursors.iter().any(|c| c.path == sub) {\n let lane = crate::subagent::session_id_from_path(&sub);\n cursors.push(Cursor {\n path: sub,\n offset: 0,\n is_main: false,\n lane,\n });\n }\n }\n }"
}
],
"instrument": "Start `csift wait @<uuid> --until tool:Read --timeout 60`, then spawn a subagent that calls Read: the wait fires on the child lane. Counting rule: one wait, one mid-wait spawn. The e2e test `p9_wait_discovers_children_spawned_mid_wait` in tests/cli/live/wait.rs pins it.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "src/live/wait.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift agents @<session> --format json AND csift search '' @<session> -t user.message --no-subagents --format json --max-count 0, joined in python (each lane's trigger_utc bisected into the sorted list of main-lane user.message ts_utc); then a live mid-wait probe on a synthetic home: csift --claude-home <TMP>/midwait/.claude wait @<fixture-uuid> --until tool:Read --timeout 30 --format json, with a helper process creating <fixture-session>/subagents/agent-<hex>.jsonl 6 s after the wait began",
"observed": "Live session (one busy session in the corpus): 79 user.message turn openers on the main lane, 109 subagent lanes carrying a trigger_utc. 109 of 109 lanes were triggered STRICTLY after the opening user record of their containing turn; 0 landed exactly at an opener and 0 before the first opener. The busiest single turn spawned 31 lanes at 26 distinct instants, from T0+321 s to T0+3095 s (a 46-minute spread inside ONE turn); 19 of the 79 turns spawned at least one lane. Mid-wait probe: at baseline csift printed 'csift: watching 1 file(s) from byte offsets; conditions: tool:Read; timeout 30s' (only the main transcript existed - the subagents directory had not been created); the child file was written at +6 s; the wait exited 0 with fired='tool:Read', waited_secs=7, activity={'lanes':1,'records':1,'tools':{'Read':1},'thinking':0,'agent_messages':0}, and an evidence row 'children: 1 lane(s), 1 live'.",
"rule": "One session for the corpus half: a lane counts as mid-turn when its trigger_utc is strictly greater than the ts_utc of the nearest preceding main-lane user.message record. One wait for the second half: exactly one child transcript created after the readiness line printed, and the awaited tool name occurs ONLY inside that child file, so a fire cannot come from baseline history.",
"note": "Both halves decided by instruments. The code site is verbatim in the current file at src/live/wait.rs:95-109 (unchanged line numbers), and the named e2e test p9_wait_discovers_children_spawned_mid_wait exists at tests/cli/live/wait.rs:140. The 109/109 count is the strongest form of the claim: in this session no subagent lane at all existed when its turn opened, so a watch set fixed at observation start would have been empty for every one of them."
}
]
},
{
"id": "MISC-022",
"area": "misc",
"behavior": "Claude Code exposes NO single field that answers 'has this session stopped': the answer must be joined from the harness session registry status, the transcript tail, the owner process, the child lanes, the elicitation sidecar and the background launches, and those surfaces disagree in a bounded number of ways.",
"depends": "csift's `assess_full` ranks them in one fixed precedence - dead process > blocked on human > tool in flight > live children > open background task under the lens > clean end of turn > unknown - and emits an `unknown` verdict with the disagreement in the evidence rows rather than guessing.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "85-89",
"snippet": "/// Join the surfaces into one verdict. Precedence (each step names its evidence):\n/// dead process > blocked-on-human > tool-in-flight > live children > open background\n/// task under the lens > clean EoT > unknown. Never guesses: a shape the rules cannot\n/// rank is `unknown` with the disagreement in the evidence rows.\npub(crate) fn assess_full("
},
{
"path": "src/live/verdict.rs",
"lines": "370-380",
"snippet": " } else if main_tail.records_seen == 0 {\n notes.push(\"no records readable at the tail\".to_string());\n Verdict::Unknown\n } else {\n notes.push(format!(\n \"tail shape unranked: no pending call, last stop_reason {} - not an end_turn, \\\n not provably running\",\n main_tail.last_stop_reason.as_deref().unwrap_or(\"(none)\")\n ));\n Verdict::Unknown\n }"
}
],
"instrument": "`csift status @<uuid> --format json | jq '.verdict, .evidence'` on a session in each state; the unit test `verdict_slugs_are_the_closed_wire_set` in src/live/tests/conditions.rs pins the wire slugs. Counting rule: one verdict per invocation; the evidence array must name every surface that answered.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "src/live/verdict.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python over ~/.claude/sessions/*.json reading status/statusUpdatedAt/updatedAt/pid plus ps -p PID -o lstart= per row; csift status @<session> --format json per row reading .verdict and .evidence[]; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c for endedAt/exitedAt/stoppedAt/finishedAt/SessionEnd and | rg -o for the registry status enum and the SessionEnd hook descriptor; csift stats @<session> --no-subagents --format json for the whole-file line-type census",
"observed": "Registry status is a closed 4-value set with no terminal member - the binary carries `var je=[\"busy\",\"shell\",\"idle\",\"waiting\"];function Ke(e){return je.includes(e)?e:void 0}` and the row parser applies it as `status:Ke(o.status)`. None of the 7 on-disk registry rows carried endedAt/exitedAt/stoppedAt/finishedAt; binary counts: exitedAt 0, stoppedAt 0, finishedAt 2, endedAt 10 (all in unrelated snapshot/daemon-upgrade code). SessionEnd exists only as a HOOK event descriptor - `SessionEnd:{summary:\"When a session is ending\",description:`Input to command is JSON with session end reason.` - i.e. a user-configured command, not a persisted field. All 7 pids were alive, yet statusUpdatedAt ages spanned 76 s to 772594 s (8.9 days) - the field is transition-written, so its age is not a liveness signal. Joined verdicts over the same 7 rows: running x3, idle-eot x3, idle-background-open x1, with 3 to 5 evidence rows each. Disagreements: 2 of 7 rows say registry 'busy' while the tail says 'no pending call; last stop_reason end_turn' (the two fields imply opposite answers), and 1 of 7 says registry 'idle' (age 772594 s) yet resolves idle-background-open because background says '1 open'. A whole-file line-type census of one transcript returned 13 distinct types (ai-title, assistant, atis-latch, attachment, bridge-session, file-history-delta, file-history-snapshot, last-prompt, mode, permission-mode, queue-operation, system, user) - none of them a session-end marker.",
"rule": "One registry row per file in ~/.claude/sessions; one csift status invocation per row. A 'disagreement' counts when the registry status field and the transcript tail imply opposite answers to 'has this session stopped' (busy/shell vs a clean end_turn with no pending call, or idle vs an unreturned background launch). Surfaces counted from the evidence array of each invocation.",
"note": "The refutation attempt (find one field that answers it) failed on every leg: the registry status enum has no terminal value and is stale by up to 8.9 days on a live pid, no ended/exited/stopped timestamp exists in the row, SessionEnd is a hook event rather than a persisted fact (and would not fire for a killed process), and the transcript itself has no end-of-session line type. Claude Code's own reader performs the same kind of join for a different purpose and can also answer 'unknown' - the binary carries `isSameProcess(d.pid, procStartFt ?? procStart)` feeding `{verdict:\"live\"|\"none\"|\"unknown\", ...unproven}`. Both code snippets are verbatim in the current file at src/live/verdict.rs:85-89 and 370-380, the six surface names (registry, pid, tail, children, sidecar, background) exist in the current code, and the unit test verdict_slugs_are_the_closed_wire_set exists at src/live/tests/conditions.rs:39 pinning 7 slugs. Limits of this run: 4 of the 7 wire verdicts were observed live (running, idle-eot, idle-background-open, plus waiting-children on the synthetic fixture of MISC-021); unknown, waiting-hitl and stale-dead were not reproduced and remain pinned only by tests, and the sidecar surface emitted no row because no probed session had an unanswered elicitation."
}
]
},
{
"id": "MISC-023",
"area": "misc",
"behavior": "On Unix, `ps -p PID -o lstart=` is not universally available: a busybox `ps` rejects the flags outright and returns failure even for a LIVE pid, while `/proc/<pid>` answers liveness directly on Linux and does not exist on macOS.",
"depends": "csift falls back to a `/proc/<pid>` directory test before declaring a pid dead and discloses the reuse guard as skipped rather than silently trusting a pid-only probe - a wrong `Dead` here turns every verdict into `stale-dead`.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "213-223",
"snippet": " if !out.status.success() || text.is_empty() {\n // busybox ps (Alpine and friends) rejects `-p`/`lstart` outright, so the probe\n // fails for a LIVE pid too. On Linux `/proc/<pid>` answers liveness directly:\n // present = alive with the start time unknown (the reuse-guard skip is\n // disclosed); absent (or no /proc at all, as on macOS where the ps form is\n // reliable) = the no-such-process verdict stands.\n if std::path::Path::new(&format!(\"/proc/{pid}\")).is_dir() {\n return PsProbe::Alive(None);\n }\n return PsProbe::NoProcess;\n }"
}
],
"instrument": "Run `ps -p $$ -o lstart=` on a busybox userland (empty output, non-zero status) versus on macOS or glibc Linux (a local-zone date). Counting rule: one probe per platform. The unit tests `probe_pid_own_process_guard_states` and `probe_pid_reports_a_reaped_pid_dead` in src/live/tests/surfaces.rs pin the alive and dead arms; only a real run on a busybox host can confirm the fallback path.",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/live/registry.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "docker run --rm busybox sh -c 'ps -p 1 -o lstart=; echo \"exit=$?\"; test -d /proc/1 && echo PROC_1_EXISTS || echo NO_PROC_1; ps --help' AND on the macOS host: ps -p $$ -o lstart=; echo exit=$?; test -d /proc/$$ ; ls -d /proc ; ps -p 999999 -o lstart=",
"observed": "BusyBox v1.38.0 userland: `ps: invalid option -- 'p'` on stderr followed by its usage line `Usage: ps [-o COL1,COL2=HEADER] [-T]`, STDOUT EMPTY, exit=1 - and the pid probed was 1, the container's live init process. `/proc/1` present in the same container (PROC_1_EXISTS). macOS (Darwin 24.6.0): `ps -p <own pid> -o lstart=` exits 0 with `Wed 2 Sep 20:08:25 2026`; `/proc` does not exist (`ls: /proc: No such file or directory`); a nonexistent pid gives `ps: process id too large: 999999` with exit=1.",
"rule": "One probe per userland. The busybox arm counts as confirmed only when the probed pid is provably alive (pid 1 of a running container) AND the probe still returns non-zero with empty stdout - that is exactly the false-dead shape the claim describes. The macOS arm counts as confirmed when /proc is absent and the ps form succeeds for a live pid and fails for a dead one.",
"note": "Code verbatim in the current file at src/live/registry.rs:209-219. Two details worth recording: busybox rejects `-p` during option parsing, before `-o lstart` is ever considered, and it writes the complaint to stderr - so the arm of csift's guard that actually fires is `text.is_empty()` on stdout, not only the exit status; and `/proc/<pid>` was confirmed present in the busybox container, so the fallback returns Alive(None) there rather than a wrong Dead. The csift binary itself is a macOS build and was not executed inside the container, so the branch is decided by the two measured preconditions rather than by observing csift's own output on that userland; running a Linux build of csift inside a busybox container and reading its pid evidence row would close that last gap."
}
]
},
{
"id": "MISC-024",
"area": "misc",
"behavior": "`ps -o lstart=` renders its date in at least two field orders - `Sun Aug 16 09:04:23 2026` and `Sun 16 Aug 09:04:23 2026` - and the order is chosen by the LC_TIME locale of the process that runs ps, not by the host family: one macOS host produced the first order under LC_ALL=C and en_US.UTF-8 and the second under en_GB.UTF-8 and en_AU.UTF-8. Claude Code avoids the ambiguity for its own registry field by pinning the environment (`LC_ALL=C TZ=UTC ps -o lstart= -p <pid>`), so procStart is always the C-locale order in UTC; a probe that inherits the caller's locale, as csift's does, sees whichever order that locale selects.",
"depends": "csift tries both strftime patterns before giving up and degrading to a disclosed pid-only probe; matching only one order silently disables the pid-reuse guard on the other family of hosts.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "224-234",
"snippet": " let local = crate::timez::local_tz();\n for fmt in [\"%a %b %e %H:%M:%S %Y\", \"%a %e %b %H:%M:%S %Y\"] {\n if let Ok(bd) = jiff::fmt::strtime::parse(fmt, &text) {\n if let Ok(dt) = bd.to_datetime() {\n if let Ok(z) = dt.to_zoned(local.clone()) {\n return PsProbe::Alive(Some(z.timestamp()));\n }\n }\n }\n }\n PsProbe::Alive(None)"
}
],
"instrument": "`for L in C en_US.UTF-8 en_GB.UTF-8 en_AU.UTF-8; do LC_ALL=$L ps -p <pid> -o lstart=; done` on a single host, then the same four locales around `csift status @<id> --format json | jq '.evidence[]|select(.surface==\"pid\").value'`. Counting rule: one rendering per locale; every order must yield `alive (start-time guard matched)`, because a strftime parse that falls through both patterns degrades to `alive (pid only)` and silently disables the pid-reuse guard.",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/live/registry.rs"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for L in C en_US.UTF-8 en_GB.UTF-8 en_AU.UTF-8; do LC_ALL=$L ps -p <live-pid> -o lstart=; done on ONE macOS host; then the same four locales wrapped around csift status @<session> --format json, reading the value of the evidence row whose surface is 'pid'; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'LC_ALL=C TZ=UTC ps -o lstart= -p '",
"observed": "Same host, same pid, four locales: LC_ALL=C -> `Tue Sep 1 01:02:24 2026`; en_US.UTF-8 -> `Tue Sep 1 01:02:24 2026`; en_GB.UTF-8 -> `Tue 1 Sep 01:02:24 2026`; en_AU.UTF-8 -> `Tue 1 Sep 01:02:24 2026`. So BOTH claimed field orders came out of one machine - the discriminator is LC_TIME, not the host family. csift's pid evidence row read `alive (start-time guard matched)` under all four locales; a parse falling through both patterns would instead read `alive (pid only)` (src/live/verdict.rs:192 vs 196), so the second pattern is load-bearing on this host's default locale. Claude Code 2.1.258 pins the environment for its own copy of this probe - the binary contains the literal `LC_ALL=C TZ=UTC ps -o lstart= -p ` - and the matching on-disk registry procStart for that pid reads `Mon Aug 31 15:02:24 2026`, the C-locale order rendered in UTC, ten hours behind the local ps rendering.",
"rule": "One rendering per locale on one host, plus one csift status invocation per locale. Both orders count as tolerated only if the pid evidence row says `start-time guard matched` under each, since a failed strftime parse silently degrades to `alive (pid only)`.",
"note": "The two-order fact holds and both patterns are exercised in practice; only the causal attribution needed fixing - it is a locale property, reproducible on one machine, not a macOS-versus-Linux property, so the claim's original instrument (compare macOS against a Linux host) could have shown one order twice and wrongly concluded the second pattern was dead code. Code verbatim in the current file at src/live/registry.rs:220-230. Follow-up worth considering for csift: pinning `LC_ALL=C TZ=UTC` on its own ps call, the way Claude Code does, would make the probe locale-independent and reduce the tolerated set to one pattern; today the guard depends on the caller's locale landing in the two-pattern set, and a locale rendering a third order (a non-English month abbreviation, for instance) would silently skip the reuse guard."
}
]
},
{
"id": "MISC-025",
"area": "misc",
"behavior": "Claude Code 2.1.258 carries ONE bundled array literal of 183 comma-separated quoted tool names, opening [\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",\"Edit\",\"MultiEdit\",\"Write\",\"NotebookEdit\",\"Glob\",\"Grep\",\"LS\",\"TodoWrite\",\"TaskCreate\",\"TaskGet\",\"TaskList\",\"TaskUpdate\",\"TaskStop\",\"TaskOutput\",\"LSP\",...] and continuing through \"Snip\", \"WebFetch\", \"WebSearch\", \"WebBrowser\", \"Agent\", \"Task\", \"Workflow\", \"Skill\", \"ScheduleWakeup\", \"SendMessage\", \"SendUserMessage\", \"SendFile\", \"Artifact\" and 105 mcp__-prefixed connector names. It is NOT a tool registry: it is the `disallowed_tools` list of a remote pull-request-review job config, and it omits first-party tool names that a separate 22-element tool array does carry (JavaScript, AskUserQuestion, ToolSearch). No single array in the binary enumerates every first-party tool; tool-name arrays are per-purpose (allow-lists, deny-lists, read-only sets).",
"depends": "csift never enumerates the registry: every scanning surface keys on a hand-picked SUBSET of tool names as byte needles (subagent spawn linkage matches exactly `Agent`/`Task`/`Workflow`, `files` admits on six needles, `recover` on eleven), so a registry member csift does not name is invisible to `files`, `recover`, `agents` spawn linkage and the `status` background section without any counter registering the loss.",
"code": [
{
"path": "src/subagent/spawn.rs",
"lines": "250-251",
"snippet": "pub(crate) fn is_spawn_tool(name: &str) -> bool {\n matches!(name, \"Agent\" | \"Task\" | \"Workflow\")"
},
{
"path": "src/recover/scan.rs",
"lines": "295",
"snippet": " static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 11]> ="
}
],
"instrument": "`strings -n 30` over the installed Claude Code binary, filtered for the array literal opening [\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\", parsed as JSON: 183 elements, of which 105 are mcp__-prefixed. A second `rg` for the surrounding bytes recovers its binding (`var ymo=`) and its use site (`disallowed_tools:[...ymo]`).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "CC=~/.local/share/claude/versions/2.1.258; strings -n 30 \"$CC\" | rg -o '\\[\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\"[^\\]]*\\]' | sort -u # then json.loads(...) for len(); plus: strings -n 30 \"$CC\" | rg -o 'var ymo=\\[\"Bash\".{0,60}' and rg -o 'mcp__claude-code-remote\"\\].{0,400}' and rg -o '\\[[^\\[\\]]{20,3000}\"TodoWrite\"[^\\[\\]]{0,3000}\\]'",
"observed": "exactly 1 distinct array literal matched; json.loads gives 183 elements and the raw text carries 182 commas (183 = commas+1); first 23 elements are Bash, BashOutput, KillShell, PowerShell, Tmux, Monitor, REPL, Read, Edit, MultiEdit, Write, NotebookEdit, Glob, Grep, LS, TodoWrite, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskStop, TaskOutput, LSP - matching the claim's opening verbatim. 105 of the 183 are mcp__-prefixed connector tool names, 78 are not. The literal is bound as `var ymo=[...]` and its single observed use is `disallowed_tools:[...ymo]` inside a job_config for a remote pull-request-review job, paired with a two-element `allowed_tools` array. A SEPARATE 22-element array literal exists: [\"Bash\",\"Read\",\"Write\",\"Edit\",\"Glob\",\"Grep\",\"NotebookEdit\",\"WebFetch\",\"WebSearch\",\"Task\",\"TodoWrite\",\"TaskCreate\",\"TaskUpdate\",\"TaskGet\",\"TaskList\",\"TaskStop\",\"Skill\",\"REPL\",\"JavaScript\",\"AskUserQuestion\",\"ToolSearch\",\"SendUserMessage\"], and 3 of its names (JavaScript, AskUserQuestion, ToolSearch) are ABSENT from the 183-element array.",
"rule": "One element per comma-separated quoted name inside the single matched array literal (verified two ways: JSON array length, and comma count + 1). Distinct-literal count after `sort -u`. Membership test: exact string equality against the parsed 183-element list.",
"note": "The 183 count and the quoted names hold verbatim, so the depends-clause consequence (csift names only a hand-picked subset of tool names as byte needles, and an unnamed tool is invisible to files/recover/agents-spawn-linkage with no counter registering the loss) is unaffected - but the population should not be called a registry. csift's code sites verify verbatim at the claimed lines: src/subagent/spawn.rs:242-243 is `pub(crate) fn is_spawn_tool(name: &str) -> bool { matches!(name, \"Agent\" | \"Task\" | \"Workflow\")`, and src/recover/scan.rs:295 is `static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 11]> =`."
}
]
},
{
"id": "MISC-026",
"area": "misc",
"behavior": "A transcript's filename basename equals the record-level `sessionId` on every observed file: over the live corpus's 67 top-level transcripts, all 67 carried at least one `sessionId` value, 0 carried a value differing from the basename, and 0 carried more than one distinct value.",
"depends": "csift derives every row's id from the FILENAME and keeps the data-derived `sessionId` only as a fallback when the filename yields nothing; the harness session-registry join in `status` matches a row by `sessionId`, so the two agreeing is what makes that join work at all.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "82-87",
"snippet": " // Prefer the filename-derived id; cross-check with the data id (§2.4 spirit).\n let session_id = if session_id.is_empty() {\n data_session_id.unwrap_or_default()\n } else {\n session_id\n };"
},
{
"path": "src/live/registry.rs",
"lines": "57-59",
"snippet": " if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) {\n continue;\n }"
}
],
"instrument": "Same procedure; the corpus size is a moving number (66 when first measured, 67 one day later - it grows by one per new session), so the reproducible part is the two zeros, not the denominator.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over every ~/.claude/projects/<encoded>/<uuid>.jsonl at depth 1: collect set(re.finditer(rb'\"sessionId\"\\s*:\\s*\"([^\"]*)\"', bytes)) per file and compare against basename[:-6]; separately python3 over ~/.claude/sessions/*.json collecting each row's sessionId and testing membership in the set of corpus transcript basenames.",
"observed": "top-level transcripts: 67. Files carrying >=1 sessionId: 67. Files with none: 0. Files whose basename is absent from their own sessionId value set: 0. Files holding more than one distinct sessionId value: 0. Harness registry: 7 .json rows, 7 carrying a non-empty sessionId, 7 of 7 resolving to a transcript basename present in the corpus.",
"rule": "One observation per top-level transcript file (a project dir's direct *.jsonl children only - subagent transcripts live one level down under <uuid>/subagents/ and are excluded). The regex is unescaped-JSON-key only: an escaped \\\"sessionId\\\" quoted inside prose does not match, so a value set of size 1 means the key never appears with a foreign value at any nesting depth. One observation per harness registry row for the join test.",
"note": "Number corrected 66 -> 67; the invariant itself is unchanged and the two failure counts are still 0. The depends-clause was instrumented directly rather than inferred: every harness registry row's sessionId resolved to a corpus transcript basename (7/7), which is exactly the join src/live/registry.rs:57-59 performs. Both csift code sites verify verbatim at the claimed lines - src/session/summarize.rs:82-87 (`// Prefer the filename-derived id; cross-check with the data id (SS2.4 spirit).` followed by the `if session_id.is_empty()` fallback) and src/live/registry.rs:57-59 (`if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) { continue; }`)."
}
]
},
{
"id": "MISC-027",
"area": "misc",
"behavior": "Claude Code's `SessionStart` hook matcher accepts exactly five source values - the array literal `[\"startup\",\"resume\",\"clear\",\"compact\",\"fork\"]` - so `resume` and `fork` are first-class session-start causes distinct from `compact`, and the firing source is handed to the hook as `.source` on its stdin payload.",
"depends": "csift's #1 hook recipe registers `SessionStart` with matcher `compact` and re-checks `.source` inside the script before injecting the verbatim turns a compaction clipped, so it fires on a compaction only; the same script under matcher `resume` would fire on a resume, and the separate `fork` source is what distinguishes a fork from a resume at hook time - a distinction the transcript itself does not carry.",
"code": [
{
"path": "SKILL.md",
"lines": "526",
"snippet": "in=$(cat); [ \"$(jq -r '.source//empty' <<<\"$in\")\" = compact ] || exit 0"
},
{
"path": "SKILL.md",
"lines": "534",
"snippet": "Register N times: `{\"matcher\":\"compact\",\"hooks\":[{\"type\":\"command\",\"command\":\"/ABS/csift-turns-slice.sh i\"}]}`, i=1..N. Absolute path (a relative command resolves against the hook child's cwd, not the skill's install dir; `$CLAUDE_PROJECT_DIR` is set for every hook child, global registrations included, so it is not the reason); `--window 9000` stays under the 10K additionalContext cap."
}
],
"instrument": "`strings -n 20` over the installed Claude Code binary, filtered for an array literal starting `[\"startup\",\"resume\",\"clear\",\"compact\"` - one distinct occurrence, `[\"startup\",\"resume\",\"clear\",\"compact\",\"fork\"]`. Counting rule: exact array literal, deduplicated.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "CC=~/.local/share/claude/versions/2.1.258; strings -n 20 \"$CC\" | rg -o '\\[\"startup\",\"resume\",\"clear\",\"compact\"[^\\]]*\\]' | sort -u; strings -n 20 \"$CC\" | rg -o '.{0,150}\"SessionStart\".{0,150}' | sort -u",
"observed": "Exactly one distinct array literal: [\"startup\",\"resume\",\"clear\",\"compact\",\"fork\"]. Its schema context is `hook_event_name:x(\"SessionStart\"),source:ee([\"startup\",\"resume\",\"clear\",\"compact\",\"fork\"]),agent_type:i().optional(),model:i().optional(),session_title:i().optional(),seconds_since_l...`. The payload builder reads `j={...,hook_event_name:\"SessionStart\",source:n,agent_type:d,model:f,session_title:o??fu(U.id),...I}` and passes the same value as `matchQuery:n` to the hook runner, so the matcher matches on the source value and the same value is handed to the hook as `source`.",
"rule": "Exact array literal, deduplicated with `sort -u`: one distinct occurrence. Payload-field presence read from the one matched builder expression.",
"note": "Confirmed in both directions: the five-value enum is the schema for the `source` field AND the value is passed as `matchQuery`, which is what makes `{\"matcher\":\"compact\"}` select on it. The builder also carries `agent_type`, `model` and `session_title` alongside `source` - fields the claim does not mention but which do not affect it. csift's SKILL.md sites verify verbatim at the claimed lines: line 526 is `in=$(cat); [ \"$(jq -r '.source//empty' <<<\"$in\")\" = compact ] || exit 0` and line 534 is the `Register N times: {\"matcher\":\"compact\",...}` sentence."
}
]
},
{
"id": "MISC-028",
"area": "misc",
"behavior": "Claude Code accepts a `--fork-session` flag (registered help: `When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)`; guidance string: `Add --fork-session to branch off a copy instead.`). It does NOT write a `forkSession` record field: the sentence `forkSession follows across the compaction break. Distinct from the session-file chain parent (which is the post-compact summary). Absent from older producers.` is the `.describe()` text of a schema field named `logical_parent_uuid` - i.e. it documents the backpointer the fork OPERATION follows, and `forkSession` there is the operation's name. The fork validators require `sessionId` to be a UUID and, when supplied, `upToMessageId` to be a UUID (optional). 0 of the live corpus's 67 top-level transcripts carry the key `forkSession` - consistent with no such record field existing. A fork does leave an on-disk trace, in the ORIGIN file: a `{\"type\":\"continued-in\",\"sessionId\":<origin>,\"continuedInSessionId\":<successor>}` record naming the successor session.",
"depends": "csift models the compaction-fork CLONE (a byte-copied head whose first timestamped record is a `compact_boundary`, joined to its origin by that boundary uuid) but not `forkSession`; a forked session's relationship to its origin is therefore not surfaced by `list`, and a consumer cannot distinguish an ordinary resume from a fork.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "186-189",
"snippet": "pub(crate) fn clone_origin(path: &Path, boundary_uuid: &str) -> Option<String> {\n let dir = path.parent()?;\n let finder = memchr::memmem::Finder::new(boundary_uuid.as_bytes());\n let entries = std::fs::read_dir(dir).ok()?;"
}
],
"instrument": "`strings -n 20` over the installed binary for the describe() text, the flag registration and the two validator messages; then a byte scan of every top-level transcript for `forkSession` (0 of 67) and an rg for `{\"type\":\"continued-in\"` (1 of 67, its successor id resolving to a sibling transcript).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "CC=~/.local/share/claude/versions/2.1.258; strings -n 20 \"$CC\" | rg -o '.{0,200}forkSession follows across.{0,200}' | sort -u; strings -n 20 \"$CC\" | rg -o '.{0,80}--fork-session.{0,120}' | sort -u; strings -n 20 \"$CC\" | rg -o '.{0,120}upToMessageId.{0,160}' | sort -u; python3 byte-scan of every top-level ~/.claude/projects/<encoded>/<uuid>.jsonl for the key \"forkSession\"; rg -o '\\{\"type\":\"continued-in\"[^}]*\\}' --glob '*.jsonl' over ~/.claude/projects.",
"observed": "(a) The sentence is the `.describe()` argument of a schema field literally named `logical_parent_uuid`: `logical_parent_uuid:Y().nullable().optional().describe(\"@internal uuid of the last pre-compact message - the backpointer \"+\"forkSession follows across the compaction break. Distinct from the session-file chain parent (which is the post-compact summary). Absent from older producers.\")`. (b) The flag is registered as `.option(\"--fork-session\",\"When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)\",()=>!0)`, and the guidance string `Add --fork-session to branch off a copy instead.` is present. (c) Validators: `if(!Wf(r))throw new k(...,\"forkSession: invalid sessionId (must be a UUID)\");if(n.upToMessageId&&!Wf(n.upToMessageId))throw new k(...,\"forkSession: invalid upToMessageId (must be a UUID)\")` - upToMessageId is guarded by a truthiness test, so it is optional. (d) 0 of 67 top-level transcripts contain the key \"forkSession\". (e) 1 file in the whole corpus carries a record of type `continued-in`, shaped `{\"type\":\"continued-in\",\"timestamp\":\"2026-09-02T09:54:22.256Z\",\"sessionId\":<origin>,\"continuedInSessionId\":<successor>}`; the successor id resolves to a sibling transcript file in the same project dir. The binary declares the same shape as a schema (`type:x(\"continued-in\"),continuedInSessionId:i()`) and lists it in a merge-policy table as `\"continued-in\":\"always\"`.",
"rule": "One observation per extracted string on the binary half. One observation per file on the corpus half: 67 top-level transcripts scanned, 0 containing the byte sequence \"forkSession\", 1 containing a `continued-in` record. Successor-link resolution counted as a filename-basename match anywhere under ~/.claude/projects.",
"note": "Two corrections. First, `forkSession` is an operation/option name, not a record key - the described field is `logicalParentUuid`, which csift already models and renders on compact_boundary records, so the ledger's framing overstated the gap. Second, the depends-clause line 'a consumer cannot distinguish an ordinary resume from a fork' is partly refuted: an ordinary resume reuses the session id and appends to the same file (no successor to point at), whereas the observed fork minted a new id and wrote a `continued-in` successor link into the origin. That link is a real, unmodeled csift gap - `list` does not surface it, and csift has no `continued-in` leaf. Caveat on strength: the single corpus instance came from a fork probe already present in the corpus, not from a fork I re-ran, so the trigger is inferred from the record's own shape and its schema name rather than observed at dispatch; running `claude --resume <id> --fork-session` and re-scanning the origin would settle whether `continued-in` is fork-specific or is also written on a background handoff. src/session/summarize.rs:186-189 verifies verbatim (`pub(crate) fn clone_origin(path: &Path, boundary_uuid: &str) -> Option<String>` and the three lines under it)."
}
]
},
{
"id": "MISC-029",
"area": "misc",
"behavior": "A tool-call record's only STRUCTURAL appearance of the tool NAME on the raw jsonl line is the `name` field of its `tool_use` block: measured over one project dir, 7764 of 7974 single-tool_use assistant records (97.4%) contain the name exactly once, the remainder being incidental content matches. The name is NOT mirrored on the result side - only 678 of 7986 single-tool_result carriers (8.5%) contain the tool name at all, so a result carrier is reachable only via the tolerant user-role matcher, not via a name needle. A tool whose name shares no substring with a scanner's needle set therefore leaves its tool_use line unreachable before any parse happens.",
"depends": "`csift files`' pre-JSON prefilter admits a line on exactly six needles - `Edit`, `Write`, `Bash`, `filePath`, `file-history-snapshot`, `is_error` - plus the tolerant user-role matcher; a renamed or new mutating tool whose name contains none of those and whose input key is neither `filePath` nor an error carrier is rejected pre-parse, so its mutations never reach the extraction step and never reach the malformed or dropped counters either.",
"code": [
{
"path": "src/files/run.rs",
"lines": "175-180",
"snippet": "pub(crate) fn line_is_files_candidate(line: &[u8]) -> bool {\n // R13: the genuine-user hook is serialization-tolerant (user-only - assistant\n // coverage rides the tool-name needles below, so admitting every assistant\n // text record here would repeal this prefilter). Finders built ONCE (per-line\n // hot path - the stateless form rebuilt its searcher every call).\n static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 6]> ="
},
{
"path": "src/files/run.rs",
"lines": "183-188",
"snippet": " memmem::Finder::new(b\"Edit\"),\n memmem::Finder::new(b\"Write\"),\n memmem::Finder::new(b\"Bash\"),\n memmem::Finder::new(b\"filePath\"),\n // Settings-family external writes read the snapshot version sequence.\n memmem::Finder::new(b\"file-history-snapshot\"),"
}
],
"instrument": "Read `line_is_files_candidate` in src/files/run.rs and count the memmem::Finder::new entries inside its LazyLock array (6), then test each name in a tool-name array for case-sensitive containment of one of those needles. Reported against two populations because no single binary array enumerates every first-party tool: the 183-element remote-job disallow list (176 blind) and a 22-element first-party tool array (17 blind).",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/files/run.rs line_is_files_candidate"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "sed -n '173,196p' src/files/run.rs (count the memmem::Finder::new entries in the LazyLock array); then python3 containment test of every name in each extracted tool-name array against those 6 needles; then python3 over one project dir's 23 *.jsonl under ~/.claude/projects: for each assistant record holding exactly one tool_use block, count occurrences of the block's `name` value in the raw line; and the same over single-tool_result user records joined to the name by tool_use_id.",
"observed": "The array is declared `[memmem::Finder<'static>; 6]` with elements Edit, Write, Bash, filePath, file-history-snapshot, is_error, and the function ends `crate::parse::line_has_user_role_marker(line) || NEEDLES.iter().any(|f| f.find(line).is_some())`. Containment: of the 183-element array, 7 names contain one of the six needles (Bash, BashOutput, Edit, MultiEdit, Write, NotebookEdit, TodoWrite) and 176 do not; of the separate 22-element first-party tool array, 5 hit and 17 do not (Read, Glob, Grep, WebFetch, WebSearch, Task, TaskCreate, TaskUpdate, TaskGet, TaskList, TaskStop, Skill, REPL, JavaScript, AskUserQuestion, ToolSearch, SendUserMessage). Name-occurrence census over one project dir: 7974 single-tool_use assistant records examined, 7764 (97.4%) carry the tool name exactly once in the raw line; the 210 with more than one are content coincidences concentrated in Bash 125, Edit 45, Write 25, Agent 10, SendMessage 4, AskUserQuestion 1. Result side: of 7986 single-tool_result user records with a resolvable tool name, only 678 (8.5%) contain that name anywhere in the raw line.",
"rule": "One needle per memmem::Finder::new call inside the LazyLock array (6). One containment test per array name, case-sensitive Python `in`. Name-occurrence census: one observation per assistant record whose message.content holds exactly ONE tool_use block; occurrences counted as raw-byte substring hits of the name value. Result-side census: one observation per user record whose content holds exactly ONE tool_result block whose tool_use_id joins to a name harvested from the same project dir.",
"note": "The 6 needles and both code snippets verify verbatim at the claimed lines (src/files/run.rs:175-180 and :183-188). Two wording corrections. The parenthetical 'mirrored in the toolUseResult spelling' is not supported by measurement - the result carrier usually does not carry the name, which is precisely why the tolerant user-role matcher is load-bearing there. And the population called 'the 183-entry registry array' is a remote-job disallow list (see MISC-025), so the blind-spot count is reported against both arrays. The substance - a new or renamed mutating tool whose name misses all six needles is rejected pre-parse and reaches no counter - is unchanged and is the sharper number on the first-party array: 17 of 22 named first-party tools contain none of the six needles."
}
]
},
{
"id": "MISC-030",
"area": "misc",
"behavior": "Claude Code writes the artifacts a file-recovery scan needs - `Read` calls, `tool_result` bodies, integrity errors, `edited_text_file` attachments and `file-history-snapshot` lines - under distinct field spellings rather than one common key, so a recovery scanner needs one needle per spelling instead of a single structural hook.",
"depends": "`csift recover` admits a line only when one of eleven literal byte needles is present (`toolUseResult`, `Edit`, `Write`, `Read`, `Bash`, `PowerShell`, `filePath`, `file_path`, `file-history-snapshot`, `edited_text_file`, `tool_use_error`) plus the tolerant user-role matcher, and builds its per-window opaque accounting from what survives; a mutating tool outside that set never enters the disclosure, so the opaque count UNDER-reports rather than over-reports - the one direction the no-silent-loss contract forbids.",
"code": [
{
"path": "src/recover/scan.rs",
"lines": "291-295",
"snippet": "pub(crate) fn line_is_recover_candidate(line: &[u8]) -> bool {\n // R13: the genuine-user hook is serialization-tolerant (user-only, like files').\n // Finders built ONCE (per-line hot path - the stateless form rebuilt its searcher\n // every call).\n static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 11]> ="
},
{
"path": "src/recover/scan.rs",
"lines": "298-311",
"snippet": " memmem::Finder::new(b\"toolUseResult\"),\n memmem::Finder::new(b\"Edit\"),\n memmem::Finder::new(b\"Write\"),\n memmem::Finder::new(b\"Read\"),\n memmem::Finder::new(b\"Bash\"),\n // The opaque accounting counts PowerShell tool calls; an assistant\n // record carrying one matches no other needle, so without this the\n // P count silently missed the assistant-side records.\n memmem::Finder::new(b\"PowerShell\"),\n memmem::Finder::new(b\"filePath\"),\n memmem::Finder::new(b\"file_path\"),\n memmem::Finder::new(b\"file-history-snapshot\"),\n memmem::Finder::new(b\"edited_text_file\"),\n memmem::Finder::new(b\"tool_use_error\"),"
}
],
"instrument": "Read `line_is_recover_candidate` in `src/recover/scan.rs`: the array is declared `[memmem::Finder<'static>; 11]` and its elements are the eleven literals. Counting rule: one needle per `memmem::Finder::new` call inside the `LazyLock` array.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/recover/scan.rs line_is_recover_candidate"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "sed -n '288,315p' src/recover/scan.rs; then, scoped to one project dir under ~/.claude/projects: for n in '\"toolUseResult\"' '\"filePath\"' '\"file_path\"' '\"file-history-snapshot\"' 'edited_text_file' 'tool_use_error' '\"is_error\"' 'staleReadFileStateHint' 'staleRecovered'; do rg -c --no-filename -F \"$n\" *.jsonl | paste -sd+ - | bc; done",
"observed": "The array is declared `static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 11]> =` at line 295 with exactly the eleven literals toolUseResult, Edit, Write, Read, Bash, PowerShell, filePath, file_path, file-history-snapshot, edited_text_file, tool_use_error, and the function ends `crate::parse::line_has_user_role_marker(line) || NEEDLES.iter().any(|f| f.find(line).is_some())`. Corpus line counts in that one project dir (23 transcripts): \"toolUseResult\" 7986, \"filePath\" 2727, \"file_path\" 2734, \"file-history-snapshot\" 435, edited_text_file 295, tool_use_error 119, \"is_error\" 4739, staleReadFileStateHint 205, staleRecovered 112.",
"rule": "One needle per memmem::Finder::new call inside the LazyLock array (11). Corpus half: `rg -c -F` counts matching LINES per file, summed across the 23 transcripts of one project dir; a fixed-string search, so no regex interpretation.",
"note": "Both the code snippet and the eleven literals verify verbatim at the claimed lines (src/recover/scan.rs:291-295 and :298-311). The behavior claim - that the artifacts a recovery scan needs are written under distinct spellings rather than one common key - is confirmed on disk rather than only from the code: `filePath` (2727 lines) and `file_path` (2734 lines) are two genuinely distinct live spellings in the same project dir, and the freshness signals the claim's depends-clause names appear under their own third and fourth spellings (staleReadFileStateHint 205 lines, staleRecovered 112 lines). The under-report direction of the depends-clause follows from the prefilter shape and is not separately measurable without a mutating tool outside the eleven needles."
}
]
},
{
"id": "NAR-001",
"area": "narration",
"behavior": "Since at least Claude Code 2.1.170 (records on disk from 2026-06-10) the API can return a SECOND `thinking` block inside one assistant message: a short summary of the reasoning beside it, in the conversation's language. It is identical in shape to a reasoning block - measured over 91,904 real signatures, both kinds carry exactly the block key-set {type, thinking, signature} with zero variation - so nothing on the wire separates the two except that signature's contents. Presence is NOT monotone in client version: it is a server-side behavior, and long runs of later clients wrote none. Clients from 2.1.241 carry the classifier and render the block under the hint word `summarized`.",
"depends": "csift splits the leaf `agent.thinking.narration` off `agent.thinking`; without the split an API-issued summary is counted as the model's own reasoning in every `search -t`, `--count-by label` and `stats` figure, and `verbatim` would replay it as reasoning. It is also the first leaf whose dotted path another leaf prefixes, which is why `-t` selection matches by dot SEGMENT (`-t agent.thinking` selects both leaves).",
"code": [
{
"path": "src/model/narration.rs",
"lines": "3-8",
"snippet": "//! Since at least CC 2.1.170 (2026-06-10) the API can return a SECOND `thinking` block\n//! in an assistant message: a one-sentence, user-language summary of the reasoning\n//! beside it (clients >= 2.1.241 render it dim under the hint word `summarized`).\n//! Nothing distinguishes it on the wire except a tag encoded inside the base64\n//! `signature`: protobuf field path 2 -> 1 -> 8, taking the LAST length-delimited field\n//! at each level, yielding a UTF-8 string reading `narration` vs `thinking`."
},
{
"path": "src/model/taxonomy.rs",
"lines": "66-70",
"snippet": " /// `agent.thinking.narration` - a narration-tagged thinking block: an API-issued\n /// one-sentence summary of the reasoning beside it, NOT the reasoning itself. The\n /// tag hides inside the base64 `signature` (see `model/narration.rs`); the first\n /// leaf whose path is prefixed by another leaf (`-t agent.thinking` selects both).\n AgentThinkingNarration,"
}
],
"instrument": "`csift stats <target> --format json | tail -1 | jq '{narration_blocks, unknown_thinking_tags}'` counts narration BLOCKS per model; cross-check the leaf split with `csift search '' @<session> --count-by label | rg 'agent\\.thinking'`, which on one measured session read 609 `agent.thinking` and 313 `agent.thinking.narration`.",
"located": {
"claude_code": "2.1.170",
"csift": "0.9.2",
"source": "SPEC.md section 5.1; SPEC.md section 6 v0.9.2 ledger; AGENTS.md section 3.3a; CHANGELOG 0.9.2; src/model/narration.rs module doc"
},
"first_seen_claude_code": "2.1.170",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) for v in 2.1.229 2.1.234 2.1.241 2.1.251 2.1.257 2.1.258; do strings -n 6 ~/.local/share/claude/versions/$v | rg -c 'isNarrationTaggedBlock'; done (b) strings -n 6 <2.1.258 binary> | rg 'I3n=\"narration\",scn=\"summarized\"' (c) python3 narr_scan.py ~/.claude/projects # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally bucketing each block's sorted key-set and its record's `version` field (d) csift search '' @<session> --count-by label --no-subagents",
"observed": "(a) isNarrationTaggedBlock absent in 2.1.229 and 2.1.234, present in 2.1.241, 2.1.251, 2.1.257 and 2.1.258 - the client-side narration classifier is introduced at 2.1.241. (b) 2.1.258 carries `var o=2,A=1,a=8,I3n=\"narration\",scn=\"summarized\"` and the render site `e(ns,{hint:scn,children:a})`; 2.1.241 carries the same pair as `ation\",Iol=\"(summarized)\"`. The 2.1.258 classifier reads `function i(r){try{if(r.type!==\"thinking\"||!r.signature)return!1; ... return n===I3n}catch(n){if(rc().claim(\"narration_classifier_error\"))h(n);return!1}}` exported as `isNarrationTaggedBlock`. (c) on disk both kinds carry EXACTLY the same block key-set {signature, thinking, type}: 3,103 narration blocks and 88,966 reasoning blocks, zero variation; narration text is short (median 99 chars, max 626) against reasoning (median 519, max 96,225); the earliest narration-tagged block was written by CC 2.1.170 at 2026-06-10T02:10:12.644Z, and the corpus contains older transcripts (2.1.150, 2.1.156, 2.1.159 - 2,874 signatures, 0 narration). (d) one session: 609 agent.thinking + 313 agent.thinking.narration; `-t agent.thinking` selects both leaves (census under that selector prints both rows).",
"rule": "One count per thinking BLOCK, keyed by the tag its own signature decodes to; the census counts one per RECORD per surviving leaf. Client-version boundary = the oldest installed binary whose strings contain `isNarrationTaggedBlock`. Corpus is live, so block totals drift upward between runs; the narration figures were stable across two passes 15 minutes apart.",
"note": "Holds, with three wording corrections. (1) `written in the user's language` is too strong: inside ONE session whose human writes Chinese, narration blocks appeared in both Chinese and English, so the block tracks the conversation rather than a fixed user language. (2) `render it dim` - the hint WORD `summarized` and its render site are provable from the binary (`hint:scn`), but the dim styling is not decidable from strings alone; running 2.1.258 with a narration-bearing turn and inspecting the rendered attributes would decide it. (3) `Since at least 2.1.170` reads as a client-version gate; the per-writing-version census refutes that reading - 2.1.170 wrote 61 narration blocks out of 213 signatures, then 2.1.177 through 2.1.193 wrote 14,110 signatures with ZERO narration, and 2.1.196 resumed. Not drifted: CC 2.1.258 both carries the classifier and wrote 166 narration blocks in this corpus, the newest today."
}
]
},
{
"id": "NAR-002",
"area": "narration",
"behavior": "The tag separating the two thinking-block kinds is encoded inside the base64 `signature` at protobuf field path 2 -> 1 -> 8, taking the LAST length-delimited (wire type 2) field at each level; the payload decodes to the UTF-8 string `narration` or `thinking`.",
"depends": "`thinking_signature_tag` mirrors exactly that walk and `thinking_block_class` maps a decoded `narration` to `agent.thinking.narration` and every other outcome to `agent.thinking`. The LAST-field rule matches the client byte for byte, but on this corpus it is not yet load-bearing: a FIRST-field walk returned the identical tag on all 91,904 signatures (0 divergences), so today the two rules are indistinguishable from the data and only the client decoder decides which is correct. A shifted field PATH does break the decode. Every failure path - absent or empty signature, bad base64, truncated varint, unknown wire type, non-UTF-8 - degrades to untagged rather than erroring, in both the client (`default:return!1`, `catch{return}`) and csift.",
"code": [
{
"path": "src/model/narration.rs",
"lines": "26-35",
"snippet": "pub(crate) fn thinking_signature_tag(signature: Option<&str>) -> Option<String> {\n let sig = signature?;\n if sig.is_empty() {\n return None;\n }\n let bytes = crate::image::decode_base64(sig)?;\n let f2 = last_len_delimited(&bytes, 2)?;\n let f1 = last_len_delimited(f2, 1)?;\n let f8 = last_len_delimited(f1, 8)?;\n std::str::from_utf8(f8).ok().map(str::to_string)"
},
{
"path": "src/model/narration.rs",
"lines": "88-90",
"snippet": " if field == want {\n found = Some(&bytes[pos..end]);\n }"
}
],
"instrument": "Take a signature from any thinking block (`csift show @<session> --line <n> --raw | jq -r '.message.content[] | select(.type==\"thinking\") | .signature'`), base64-decode it, then walk field 2, then field 1, then field 8, taking the LAST length-delimited field at each level; the payload is the UTF-8 string `narration` or `thinking`. Counting rule: one decode per thinking block, tag read from that block's own signature. The csift unit tests under src/model/tests pin the LAST-field rule and the full degradation matrix with hand-emitted protobuf fixtures.",
"located": {
"claude_code": "2.1.170",
"csift": "0.9.2",
"source": "SPEC.md section 5.1; SPEC.md section 6 v0.9.2 ledger; AGENTS.md section 3.3a; src/model/narration.rs module doc"
},
"first_seen_claude_code": "2.1.170",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'function GFt|I3n=\"narration\"' (b) python3 narr_scan.py ~/.claude/projects # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, run twice per signature - once taking the LAST wire-type-2 field at each of the three levels and once taking the FIRST - and comparing the two decoded tags",
"observed": "(a) the 2.1.258 binary contains the decoder verbatim: `var o=2,A=1,a=8,I3n=\"narration\",scn=\"summarized\";function P3n(d){let n;try{n=atob(d)}catch{return}let e=new Uint8Array(n.length);for(let t=0;t<n.length;t++)e[t]=n.charCodeAt(t);let r=GFt(e,o);if(r===void 0)return;let i=GFt(r,A);if(i===void 0)return;return O3n(i,a)}` with the field getter `function GFt(n,r){let e;return acn(n,{onBytes(u,a){if(u===r)e=a}})?e:void 0}` - the callback ASSIGNS on every matching field, so the LAST wire-type-2 field wins - and `function O3n(n,r){let e=GFt(n,r);return e===void 0?void 0:l.decode(e)}` (UTF-8). The scanner `acn` skips wire 0/1/5 and `default:return!1`, i.e. an unknown wire type or any truncation aborts to undefined. It is called as `P3n(r.signature)`. (b) 91,904 real signatures decode under the 2 -> 1 -> 8 LAST-field walk to exactly two UTF-8 strings, `thinking` (88,801) and `narration` (3,103), with zero base64 errors, zero malformed protobuf and zero non-UTF-8 payloads.",
"rule": "One decode per thinking block, tag read from that block's own signature. Divergence rule: count a signature as divergent when the FIRST-field walk and the LAST-field walk return different strings.",
"note": "The behavior claim holds and is now confirmed at the strongest available level: the field path, the LAST-field selection rule and the two tag strings are all readable in the 2.1.258 binary itself, not merely inferred from data. Refined only because the `depends` clause asserts that a FIRST-field walk `silently collapses narration back into agent.thinking` - measured, that never happens on any of the 91,904 signatures in this corpus. The claim's protection is against a future signature that carries repeated fields, not against today's data."
}
]
},
{
"id": "NAR-003",
"area": "narration",
"behavior": "The signature's outer format-version varint is not a discriminator: the values 2, 4 and absent all appear in real data carrying both tags.",
"depends": "csift never consults the version varint and reads the tag only from the 2 -> 1 -> 8 payload; keying on the version would misclassify blocks in all three version populations at once.",
"code": [
{
"path": "src/model/narration.rs",
"lines": "16-17",
"snippet": "//! `narration` classifies as plain thinking. The outer format-version varint is NEVER\n//! consulted (values 2, 4 and absent all carry both tags in real data)."
}
],
"instrument": "Group real signatures by their outer format-version varint and cross-tabulate against the decoded 2 -> 1 -> 8 tag: each of the values 2, 4 and absent occurs with BOTH tags, so the version alone predicts nothing. Counting rule: one signature per thinking block.",
"located": {
"claude_code": "2.1.170",
"csift": "0.9.2",
"source": "SPEC.md v0.9.2 ledger; AGENTS.md section 3.3a; src/model/narration.rs module doc"
},
"first_seen_claude_code": "2.1.170",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 narr_scan.py ~/.claude/projects # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally recording every TOP-LEVEL wire-type-0 (varint) field of the decoded signature and cross-tabulating it against the decoded 2 -> 1 -> 8 tag",
"observed": "Top-level varint field 1 takes exactly two values plus absence, and every one of the three populations carries BOTH tags: field1=2 -> 50,105 thinking / 2,875 narration; field1=4 -> 1,055 thinking / 220 narration; field1 absent -> 37,641 thinking / 8 narration. (Top-level varint field 3 is 1 on every signature.) Total 91,904 signatures.",
"rule": "One signature per thinking block; a signature is binned by the set of its top-level varint fields and cross-tabbed against its decoded tag. The claim is refuted if any bin is single-tag.",
"note": "Holds corpus-wide, but the margin is thin in one cell and the claim is scope-sensitive: the field1-absent population holds only 8 narration signatures against 37,641 thinking ones, and restricted to a SINGLE project directory (443 files, 12,873 signatures) that cell was 5,251 thinking / 0 narration - i.e. inside one project the version varint DOES look like a perfect discriminator. A verifier who scopes too narrowly will wrongly conclude the varint is a discriminator, which is exactly the mistake the claim exists to prevent. csift's independence from the varint is confirmed in code: `thinking_signature_tag` reads only the wire-type-2 chain, and `last_len_delimited` skips wire-0 fields without inspecting their values."
}
]
},
{
"id": "NAR-004",
"area": "narration",
"behavior": "Token usage is reported once per API message and covers the reasoning block and its narration sibling together, so the transcript carries no per-block token split between reasoning and its summary.",
"depends": "`stats` reports `narration_blocks` as a BLOCK count per model and says the token split is unavailable; deriving a per-kind token figure would be a fabrication, so every narration quantity keeps the blocks-not-messages counting rule.",
"code": [
{
"path": "src/stats.rs",
"lines": "53-57",
"snippet": " /// Narration-tagged thinking blocks per model (`agent.thinking.narration`: an\n /// API-issued summary of the reasoning beside it). A BLOCK count only - the token\n /// split is not derivable from the jsonl (usage is per MESSAGE and covers the\n /// reasoning block and its narration sibling together).\n narration_blocks: BTreeMap<String, usize>,"
}
],
"instrument": "`csift stats <target> --format json | tail -1 | jq .narration_blocks` gives blocks per model; compare with the same scope's token map, which is keyed per API message id and deduped per message. Counting rule: narration is counted in BLOCKS; usage is counted once per message and cannot be attributed to one of the two thinking blocks it covers.",
"located": {
"claude_code": "2.1.241",
"csift": "0.9.2",
"source": "SPEC.md section 6 v0.9.2 ledger item 1; src/stats.rs field docs"
},
"first_seen_claude_code": "2.1.170",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 narr_usage.py ~/.claude/projects # groups every thinking-bearing record by message.id within one transcript, decodes each block's tag, and compares json.dumps(message.usage, sort_keys=True) across the per-block records of one message; also scans every thinking block's key-set for any key containing 'token' or 'usage'",
"observed": "3,103 assistant messages contain a narration block. 2,927 of them (94.3%) are written as exactly 2 per-block records - one carrying the reasoning block, one carrying the narration block - and 2,926 of those 2,927 carry a BYTE-IDENTICAL `message.usage` object on both records. The single exception is a streaming partial, not a per-block split: its first record reads output_tokens=3 and its last reads output_tokens=922 with `output_tokens_details.thinking_tokens: 426` - one message-level thinking total, not two block-level ones. Zero of the 91,904 thinking blocks carry any key containing 'token' or 'usage'. The remaining 176 narration messages are single-record.",
"rule": "One group per (transcript, message.id). Usage is compared as the canonical JSON of message.usage. A per-block split would require either differing usage objects that sum to the message total, or a token key on the block itself; neither occurs.",
"note": "Holds. The instrument also shows WHY the split is unavailable rather than merely absent: Claude Code writes one jsonl record per content block and repeats the identical per-MESSAGE usage object on each, so a naive per-record sum double-counts a reasoning+narration message and still yields no split. `output_tokens_details.thinking_tokens` is the closest thing to a thinking figure and it too is one number per message covering both blocks."
}
]
},
{
"id": "NAR-005",
"area": "narration",
"behavior": "The tag universe is open in principle and measured closed at exactly two values - `thinking` and `narration` - over 91,904 real signatures; a third value would be indistinguishable from reasoning to any consumer that hard-codes the pair.",
"depends": "csift treats any decoded value other than `narration` as plain `agent.thinking` (never an error) and counts the residue in `stats`' `unknown_thinking_tags`, so a new API tag value surfaces as a non-zero number without needing a csift release.",
"code": [
{
"path": "src/model/narration.rs",
"lines": "15-16",
"snippet": "//! `agent.thinking`), never an error. The tag set is open: any value other than\n//! `narration` classifies as plain thinking. The outer format-version varint is NEVER"
},
{
"path": "src/stats.rs",
"lines": "58-60",
"snippet": " /// Thinking-signature tags that decoded to something OTHER than thinking or\n /// narration - a new tag value surfaces here without a csift release.\n unknown_thinking_tags: usize,"
},
{
"path": "src/stats.rs",
"lines": "300-301",
"snippet": " Some(\"thinking\") | None => {}\n Some(_) => out.unknown_thinking_tags += 1,"
}
],
"instrument": "`csift stats <target> --format json | tail -1 | jq '{narration_blocks, unknown_thinking_tags}'` over a corpus spanning 2026-06-10 onward; a non-zero `unknown_thinking_tags` means the tag universe moved and the decoder's assumptions need re-reading. Counting rule: one per thinking block whose decoded tag is neither `thinking` nor `narration`; measured 0 over 91,904 real signatures.",
"located": {
"claude_code": "2.1.170",
"csift": "0.9.2",
"source": "SPEC.md v0.9.2 ledger; CHANGELOG 0.9.2; src/model/narration.rs module doc"
},
"first_seen_claude_code": "2.1.170",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) python3 narr_scan.py ~/.claude/projects # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, tallying the decoded tag string (b) csift stats ~/.claude/projects/<one project dir> --format json | tail -1 | jq '{unknown_thinking_tags, narration_blocks}' (c) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'isNarrationTaggedBlock'",
"observed": "(a) exactly two decoded values over 91,904 signatures: `thinking` 88,801 and `narration` 3,103; zero base64 failures, zero malformed protobuf, zero non-UTF-8, zero third value. (b) csift reports unknown_thinking_tags 0 and 996 narration blocks over one project directory - matching the independent Python decode of the same directory block for block (996). (c) the client's own test is a strict equality `return n===I3n` against the constant `\"narration\"`, so any third value falls to plain thinking there too.",
"rule": "One count per thinking block whose decoded tag is neither `thinking` nor `narration`; measured 0 over 91,904 real signatures under the projects root.",
"note": "Holds; only the sample size needed correcting - the ledger says 13,957 signatures, the current corpus carries 91,904 (the corpus is live and grows). The conclusion strengthens: a 6.6x larger sample still yields exactly two values. The claim's own escape hatch was also exercised end to end - csift's counter reads 0 and its narration total reproduces the independent decode exactly."
}
]
},
{
"id": "NAR-006",
"area": "narration",
"behavior": "Thinking-block `signature` strings reach 223,160 base64 characters (167,368 decoded bytes) in real data, and the WHOLE string must be decoded to read the tag: the protobuf scanner aborts as soon as a length-delimited field's declared length overruns the buffer, so every prefix of a signature - even 99% of it - reports untagged. The tag itself does NOT sit at the end of the byte stream (measured median 21.6%, max 42.4% of the decoded length); `LAST field` is a protobuf field-ordering rule at each nesting level, not a byte position.",
"depends": "csift decodes the WHOLE signature string; any windowed decode under-reports `agent.thinking.narration` with no error and no dropped-count disclosure - a failure a narration figure cannot detect from its own output.",
"code": [
{
"path": "src/model/narration.rs",
"lines": "11-13",
"snippet": "//! entirely (~4% measured), so adjacency is never a shortcut. Signatures reach 200K+\n//! base64 chars; the WHOLE string is decoded (a prefix decode silently reports every\n//! long signature untagged). Every failure path - absent/empty signature, bad base64,"
}
],
"instrument": "`rg -o '\"signature\":\"[^\"]*\"' <transcript> | awk '{print length-14}' | sort -n | tail -1` for the maximum base64 length (the 14 subtracts the `\"signature\":\"` key and the closing quote); measured 223,160 characters. Counting rule: one measurement per thinking-block signature string. Re-run any narration count after changing the decode window, since the difference shows up only as a lower narration total.",
"located": {
"claude_code": "2.1.170",
"csift": "0.9.2",
"source": "SPEC.md v0.9.2 ledger; AGENTS.md section 3.3a; CHANGELOG 0.9.2; src/model/narration.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) rg -o '\"signature\":\"[^\"]*\"' <one transcript> | awk '{print length}' | sort -n | tail -1 (b) python3 narr_scan.py ~/.claude/projects # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally recording, for every narration signature, the byte offset of the tag payload inside the DECODED buffer as a fraction of the decoded length (c) the same decoder run on character prefixes (10/25/50/75/90/99/100%, truncated to a multiple of 4) of the longest narration signature",
"observed": "(a) the longest `\"signature\":\"...\"` match in the one file holding it is 223,174 characters; minus the 14-character `\"signature\":\"` + closing-quote wrapper that is 223,160 base64 characters (167,368 decoded bytes). That maximal signature is `thinking`-tagged; the longest NARRATION signature in the whole corpus is only 3,536 characters. (b) over 3,103 narration signatures the tag payload's byte offset inside the decoded buffer is min 0.57%, median 21.6%, max 42.4% of the decoded length - it is never in the last 10%. (c) prefixes of the longest narration signature at 10, 25, 50, 75, 90 and 99% of its characters ALL decode to no tag; only the full 3,536-character string decodes to `narration`.",
"rule": "One measurement per thinking-block signature string. Offset rule: index of the tag bytes in the base64-decoded buffer divided by that buffer's length. Prefix rule: truncate the base64 string to floor(n*frac/4)*4 characters and re-run the identical 2 -> 1 -> 8 walk.",
"note": "The operational conclusion holds and was tested directly - a windowed decode silently reports untagged with no error - but both the number and the stated mechanism needed correcting. The number is stale (200,356 -> 223,160; the corpus is live). The mechanism sentence `the tag sits at the END of the decoded structure` is refutable: the tag is at roughly the first quarter of the decoded bytes and never in the last tenth. The real reason a prefix decode fails is that the scanner treats any truncation as malformed and aborts to untagged, which is what the prefix ladder shows. Note also that the 200K-class signatures are reasoning blocks, not narration ones, so the cost the alignment gate avoids is dominated by signatures that can never be narration."
}
]
},
{
"id": "NAR-007",
"area": "narration",
"behavior": "The base64 alignment of the tag bytes MIXES inside one transcript: the token `narration` appears at all three stream offsets (0, 1, 2 mod 3), encoded as `bmFycmF0aW9u`, `5hcnJhdGlv` or `uYXJyYXRp`, so no single-alignment substring test is sound.",
"depends": "csift gates the expensive full decode behind all three alignment needles - a microsecond substring check instead of decoding a 200 KB signature on every thinking block of every candidate record; the ungated form cost about 20% more wall time on a 685 MB transcript, and dropping any one needle loses tagged blocks.",
"code": [
{
"path": "src/model/narration.rs",
"lines": "38-46",
"snippet": "/// The three base64 alignments of the tag bytes (`narration` at stream offset 0/1/2\n/// mod 3). A signature containing NONE of them cannot decode to `narration`, so the\n/// hot path skips the full decode - a µs substring check instead of decoding a 200KB\n/// signature (classify runs on every thinking block of every candidate record; the\n/// ungated form cost ~+20% wall on a 685MB transcript). Verified sound on 13,957 real\n/// signatures (alignment MIXES within one file - all three needles are required).\n/// Whitespace inside the base64 (tolerated by the decoder, never observed in real\n/// signatures) would break needle adjacency, so it conservatively re-opens the gate.\nconst NARRATION_B64_ALIGNMENTS: [&str; 3] = [\"bmFycmF0aW9u\", \"5hcnJhdGlv\", \"uYXJyYXRp\"];"
},
{
"path": "src/model/narration.rs",
"lines": "51-56",
"snippet": "pub(crate) fn thinking_block_class(signature: Option<&str>) -> Class {\n let Some(sig) = signature else {\n return Class::AgentThinking;\n };\n let gate_open = NARRATION_B64_ALIGNMENTS.iter().any(|n| sig.contains(n))\n || sig.bytes().any(|b| b.is_ascii_whitespace());"
}
],
"instrument": "Count each needle separately over one transcript (`rg -c 'bmFycmF0aW9u' <transcript>` and the same for `5hcnJhdGlv` and `uYXJyYXRp`): more than one alignment fires inside a single file. Soundness check: for every real signature run both the full 2 -> 1 -> 8 decode and the three-needle gate and assert the gate never closes on a signature the decode tags `narration` - asserted over 13,957 real signatures. Counting rule: one classification per signature. Removing the gate must leave output byte-identical while costing roughly 20% more wall time on a 685 MB transcript, same query and warm cache.",
"located": {
"claude_code": "2.1.241",
"csift": "0.9.2",
"source": "SPEC.md v0.9.2 ledger; AGENTS.md section 3.3a; CHANGELOG 0.9.2; src/model/narration.rs alignment comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) for n in bmFycmF0aW9u 5hcnJhdGlv uYXJyYXRp; do rg -c \"$n\" <one transcript>; done (b) python3 narr_scan.py ~/.claude/projects # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally recording which of the three needles each signature contains, and asserting that no signature the decode tags `narration` lacks all three",
"observed": "(a) inside ONE 88,275,010-byte transcript all three alignments occur: bmFycmF0aW9u on 179 lines, 5hcnJhdGlv on 13, uYXJyYXRp on 340. (b) corpus-wide the three needles fire 2,664 (uYXJyYXRp) + 287 (bmFycmF0aW9u) + 152 (5hcnJhdGlv) = 3,103 times, exactly the narration total; 10 files carry more than one alignment and 2 files carry all three. Soundness: 0 of 3,103 narration-tagged signatures lack all three needles, so the gate never closes on a tagged block. Precision is also perfect here: 0 of the 88,801 `thinking`-tagged signatures contain any needle.",
"rule": "One classification per signature. Gate-soundness rule: count signatures whose full 2 -> 1 -> 8 decode reads `narration` while none of the three needles is a substring of the base64 - the claim is refuted by any non-zero count. Mixing rule: a file counts as mixed when two or more distinct needles occur in it.",
"note": "The behavior claim - all three alignments occur, and they MIX inside one transcript - is confirmed with the claim's own per-needle rg instrument, so no single-alignment substring test is sound. Two caveats on the `depends` clause, which I did not re-measure. First, the `~+20% wall on a 685MB transcript` figure was not reproduced: it needs an A/B of two release builds differing only in the gate, warm cache, same query, and the largest single transcript on this machine is now 720,932,882 bytes, so the original file has grown and the number would not be comparable anyway. Second, the gate's PRECISION (zero false opens today) means the measured saving is close to the maximum achievable, which is consistent with the claim's direction."
}
]
},
{
"id": "NAR-008",
"area": "narration",
"behavior": "Real signatures carry no embedded whitespace inside the base64, though the decoder accepts it; whitespace would split the tag bytes and break the adjacency the alignment needles depend on.",
"depends": "csift's gate opens on any needle hit OR on any ASCII whitespace byte in the signature, so a hypothetical whitespace-bearing signature falls through to the full decode instead of being silently skipped - the gate is conservative in the direction that cannot lose a tagged block.",
"code": [
{
"path": "src/model/narration.rs",
"lines": "44-46",
"snippet": "/// Whitespace inside the base64 (tolerated by the decoder, never observed in real\n/// signatures) would break needle adjacency, so it conservatively re-opens the gate.\nconst NARRATION_B64_ALIGNMENTS: [&str; 3] = [\"bmFycmF0aW9u\", \"5hcnJhdGlv\", \"uYXJyYXRp\"];"
},
{
"path": "src/model/narration.rs",
"lines": "55-56",
"snippet": " let gate_open = NARRATION_B64_ALIGNMENTS.iter().any(|n| sig.contains(n))\n || sig.bytes().any(|b| b.is_ascii_whitespace());"
}
],
"instrument": "Extract every signature string from a transcript and scan each for an ASCII whitespace byte: zero in the measured corpus. Counting rule: one per signature string. The gate's whitespace arm is the fallback that keeps the needle check sound if that ever changes.",
"located": {
"claude_code": "2.1.241",
"csift": "0.9.2",
"source": "AGENTS.md section 3.3a; CHANGELOG 0.9.2; src/model/narration.rs alignment comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 narr_scan.py ~/.claude/projects # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally testing `any(c.isspace() for c in signature)` on every extracted signature string",
"observed": "0 of 91,904 signature strings contain an ASCII whitespace character. Block shapes are uniform: 0 thinking blocks have an absent `signature` key and 35 have an empty-string one, and every non-empty signature is unbroken base64.",
"rule": "One test per signature string, over every thinking block under ~/.claude/projects; the claim is refuted by any signature containing a whitespace byte.",
"note": "Holds. The conservative arm is present in the code as claimed (`|| sig.bytes().any(|b| b.is_ascii_whitespace())` re-opens the gate), so a whitespace-bearing signature would fall through to the full decode instead of being skipped. Worth recording that the arm is currently dead in the measured sense - it never fires - which is the point: it is insurance against a serializer change, not a live path. A transcript rewritten by a JSON pretty-printer would be the case that exercises it."
}
]
},
{
"id": "NAR-009",
"area": "narration",
"behavior": "About 5.7% of narration blocks (176 of 3,103 measured) have no reasoning sibling in the same assistant message at all, so a narration block cannot be identified by adjacency to a reasoning block. Adjacency is worse than the figure suggests, because Claude Code writes one jsonl record per content block: measured per RECORD rather than per message id, 100% of narration blocks look sibling-less.",
"depends": "csift classifies from the block's OWN signature and never from position; an adjacency heuristic mislabels the sibling-less population, and a record holding both kinds splits per BLOCK rather than per record.",
"code": [
{
"path": "src/model/narration.rs",
"lines": "10-11",
"snippet": "//! Classification is by SIGNATURE ONLY - a narration block can lack a reasoning sibling\n//! entirely (~4% measured), so adjacency is never a shortcut. Signatures reach 200K+"
},
{
"path": "src/search/hits.rs",
"lines": "373-375",
"snippet": " // Signature-only split (narration vs reasoning); adjacency is never\n // consulted, and a mixed multi-block record splits per BLOCK.\n let class = crate::model::thinking_block_class(signature.as_deref());"
},
{
"path": "src/model/classify.rs",
"lines": "206-208",
"snippet": " Block::Thinking { signature, .. } => {\n push_unique(out, thinking_block_class(signature.as_deref()));\n }"
}
],
"instrument": "`csift search '' @<session> -t agent.thinking.narration --format json | jq -r .line` names each narration record; for each, `csift show @<session> --line <n> --raw | jq '[.message.content[].type]'` shows the blocks on THAT RECORD, which is not the same question - group records by `message.id` first. Counting rule: narration blocks whose assistant message carries no thinking block of any other kind, over all narration blocks; measured 5.67% (176/3,103).",
"located": {
"claude_code": "2.1.170",
"csift": "0.9.2",
"source": "SPEC.md section 5.1; SPEC.md section 6 v0.9.2 ledger item 1; AGENTS.md section 3.3a; src/model/narration.rs module doc"
},
"first_seen_claude_code": "2.1.170",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 narr_usage.py ~/.claude/projects # groups every thinking-bearing record by message.id within one transcript, decodes each block's tag, and counts narration messages whose block set contains no `thinking`-tagged block, no unsigned thinking block and no redacted_thinking block",
"observed": "176 of 3,103 narration blocks (5.67%) sit in an assistant message with NO other thinking block of any kind - not ~4%. The other 2,927 (94.3%) share a message with exactly one reasoning block, and that message is written as 2 separate jsonl records. Because of that per-block record split, a per-RECORD adjacency test is not merely unreliable but usually wrong: run per record instead of per message.id over one project directory, it reported 996 of 996 narration blocks (100%) as sibling-less.",
"rule": "Group by (transcript, message.id); a narration block counts as sibling-less when its message's thinking-block set contains no block that decodes to `thinking`, no thinking block with an empty signature, and no redacted_thinking block. Denominator = all narration blocks.",
"note": "Holds in substance - adjacency is never a shortcut - but the number and the unit both needed fixing. 4% -> 5.67%, and `in the same assistant message` has to be spelled out as `grouped by message.id`, because the obvious reading (same jsonl record) gives 100% and would make the claim look far stronger than it is. The corrected figure is the one that stresses csift's design: even at the message level, one narration block in eighteen has nothing to be adjacent to."
}
]
},
{
"id": "NAR-010",
"area": "narration",
"behavior": "Because the tag is base64-hidden inside `signature`, a narration record's raw jsonl bytes never spell `narration` as a consequence of BEING narration - the tag appears only as one of three base64 alignments. The record's own thinking text can still contain the words incidentally: measured 20 of 3,103 narration records (0.64%) carry `narration` or `summary` somewhere outside the signature. In a transcript holding 313 narration records and no such prose, `rg -c narration` returns 0.",
"depends": "csift renders `[narration summary]` in the display LABEL zone only, never as matchable text - matchable text that is not a byte substring of the raw line breaks the search prefilter laws, so `csift search narration -t agent.thinking.narration` would return zero while rows visibly rendered the marker. The same display-only status is why the fixed `--siblings` policy caps narration at 1 per turn (one summary suffices).",
"code": [
{
"path": "src/search/render.rs",
"lines": "42-45",
"snippet": " // A narration record is an API summary, never the model's reasoning - the marker\n // rides the LABEL zone (display-only, like [error]); matchable text stays verbatim.\n if h.class == Class::AgentThinkingNarration {\n return format!(\"{} [narration summary]\", h.class.path());"
},
{
"path": "src/search/turns_match.rs",
"lines": "239-241",
"snippet": " if class == Class::AgentThinkingNarration {\n return Some(1);\n }"
}
],
"instrument": "`rg -cNI 'narration' ~/.claude/projects -g '*.jsonl'` finds only prose mentions, never a signature-derived one, while `csift search '' <project-dir> --count-by label | rg narration` reports the real leaf count; the two numbers are unrelated by construction. Counting rule: lines for the `rg`, records per leaf for the census.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.2",
"source": "AGENTS.md section 3.3a; dev session measurement 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) rg -c 'narration' <one transcript holding 313 narration records> (b) csift search 'narration' @<session> -t agent.thinking.narration --no-subagents -c; csift search '\\[narration summary\\]' @<session> --no-subagents -c; csift search 'narration' @<session> --no-subagents -c (c) csift search '' @<session> -t agent.thinking.narration --no-subagents --max-count 2 (d) python3 narr_scan.py ~/.claude/projects # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally testing each narration record's RAW line for the byte strings narration / Narration / summary / Summary",
"observed": "(a) 0 lines match in a transcript that carries 313 narration records. (b) all three searches return 0, including the literal marker text. (c) every rendered hit prints `agent.thinking.narration [narration summary]` in the label zone, so the marker is visible while matching nothing. (d) corpus-wide, however, 20 of 3,103 narration records (0.64%) DO contain one of those words in the raw line - `summary` 10, `Summary` 1, `narration` 9 - always in the record's own prose, never from the signature (the tag's only byte form in the line is one of the three base64 alignments).",
"rule": "Lines for the rg; matched exchanges for `csift search -c`; records for the label census. Word rule: case-sensitive byte search of the whole raw jsonl line of each narration record, with the `\"signature\":\"...\"` values blanked out first to prove the hit is not signature-derived.",
"note": "The operational point holds exactly as designed and was verified three ways: the rendered `[narration summary]` marker matches nothing, so the display-only rule is real and the search prefilter laws are intact. Refined because the absolute wording - `no substring of the line spells narration or summary` - is refutable at 0.64% of narration records, and the counterexamples come from sessions that discuss the feature (this repository's own dev transcripts). The correct statement is about provenance: nothing signature-derived is matchable; incidental prose is."
}
]
},
{
"id": "NAR-011",
"area": "narration",
"behavior": "Narration-tagged thinking blocks do occur in subagent transcripts, but are orders of magnitude rarer there than in top-level ones: in one measured corpus, 24 narration records across 5 of ~7,543 subagent transcripts (0.07% of files) versus 3,079 across 22 of 66 top-level transcripts (33% of files). Occurrence is bursty and not lane- or model-gated - all five carrying transcripts belong to a single parent session, whose 10 other subagent transcripts run the same model id and carry none. Both built-in and teammate subagent shapes are attested.",
"depends": "csift runs the identical signature classifier on every transcript it reads, subagent files included, so the rarity is a property of the data rather than a scope gap: the subagent-resident narration records are counted with no code change, and on any spanning surface the spanned-minus-restricted difference is exactly the subagent-resident count.",
"code": [
{
"path": "src/stats.rs",
"lines": "289-291",
"snippet": " if let Block::Thinking { signature, .. } = b {\n match crate::model::thinking_signature_tag(signature.as_deref()).as_deref() {\n Some(crate::model::NARRATION_TAG) => {"
},
{
"path": "src/model/narration.rs",
"lines": "48-59",
"snippet": "/// The leaf for a thinking block: `agent.thinking.narration` iff the signature tag reads\n/// exactly `narration`; every other outcome (tag `thinking`, an unknown tag, no tag)\n/// stays `agent.thinking`.\npub(crate) fn thinking_block_class(signature: Option<&str>) -> Class {\n let Some(sig) = signature else {\n return Class::AgentThinking;\n };\n let gate_open = NARRATION_B64_ALIGNMENTS.iter().any(|n| sig.contains(n))\n || sig.bytes().any(|b| b.is_ascii_whitespace());\n if !gate_open {\n return Class::AgentThinking;\n }"
},
{
"path": "src/model/classify.rs",
"lines": "206-208",
"snippet": " Block::Thinking { signature, .. } => {\n push_unique(out, thinking_block_class(signature.as_deref()));\n }"
}
],
"instrument": "`csift search '' -t agent.thinking.narration --count-by label` run with and again with `--no-subagents`: the difference is the subagent-resident narration record count (24 here, not 0). Cross-check per transcript with `--format json` and a count of hits whose `is_subagent` is true, and independently of csift by base64-decoding each thinking block's signature and reading protobuf path 2 -> 1 -> 8. Counting rule: one per record per leaf; subagent transcripts are the files under `<uuid>/subagents/` inside each project directory, excluding journal.jsonl.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.2",
"source": "dev session measurement 2026-09-01"
},
"first_seen_claude_code": "2.1.251 (the client version stamped on all 24 attested subagent narration records)",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "1) corpus census, subagents spanned vs restricted:\n csift search '' -t agent.thinking.narration --count-by label\n csift search '' -t agent.thinking.narration --count-by label --no-subagents\n2) per-transcript split of the same scan:\n csift search '' -t agent.thinking.narration --format json \\\n | python3 -c \"import sys,json;h=[x for l in sys.stdin for x in json.loads(l).get('hits',[])];\\\nprint(len(h), sum(1 for x in h if x['is_subagent']), len({x['session_id'] for x in h if x['is_subagent']}))\"\n3) independent signature decode (does not use csift): a 40-line python protobuf reader\n walking base64(signature) -> last length-delimited field 2 -> 1 -> 8 -> utf-8, run over\n the subagent transcripts of the one session that has any:\n python3 nardec.py ~/.claude/projects/<project-dir>/<session-uuid>/subagents/agent-*.jsonl\n4) independent byte prefilter on the same files (the three base64 alignments of the tag):\n rg -c 'bmFycmF0aW9u|5hcnJhdGlv|uYXJyYXRp' <each subagent transcript>\n5) the code site's own counter:\n csift stats @<session-prefix> --format json (and again with --no-subagents)\n6) corpus shape:\n find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl | wc -l\n find ~/.claude/projects -maxdepth 2 -name '*.jsonl' | wc -l\n7) code site check:\n awk 'NR>=289 && NR<=291' src/stats.rs",
"observed": "(1) 3103 records under agent.thinking.narration with subagents spanned; 3079 with --no-subagents. Difference = 24 narration records living in subagent transcripts, so the two figures are NOT equal. (2) 3103 hits total, 24 with is_subagent true, spread over 5 distinct subagent transcripts under a single parent session; the remaining 3079 sit in 22 top-level transcripts. Hit timestamps on the subagent side run 2026-09-01T01:07 to 2026-09-01T18:24 local. (3) independent decoder over the 15 subagent transcripts of that session: tag census {'thinking': 187, 'narration': 24}; every narration-tagged record carries version 2.1.251. Per file: 2, 2, 6, 1 and 13 narration blocks in five transcripts, zero in the other ten. Four of the five are bare built-in subagent ids, one is a name-embedded teammate id. (4) rg needle counts on those same five files: 2, 2, 6, 1, 13 = 24, matching the decoder line for line. (5) csift stats on that parent session: narration_blocks total 30 with subagents spanned (16 sessions in scope) vs 6 with --no-subagents (1 session) -> 24 in subagent transcripts, matching (1) and (3). (6) 7543 subagent transcripts and 66 top-level transcripts in the corpus at measurement time (the live corpus grew by 2 subagent transcripts during the ten minutes of this verification, so treat 7.5k as the order of magnitude, not a fixed constant). (7) src/stats.rs lines 289-291 still read verbatim: 'if let Block::Thinking { signature, .. } = b {' / 'match crate::model::thinking_signature_tag(signature.as_deref()).as_deref() {' / 'Some(crate::model::NARRATION_TAG) => {'. Binary corroboration: strings -n 6 on Claude Code 2.1.258 yields the standalone symbols 'narrationBlockCount', 'narration_generate' and 'narration_classifier_error', i.e. narration is first-class machinery in the current client.",
"rule": "A subagent transcript is a *.jsonl under <session-uuid>/subagents/ inside a project directory, excluding journal.jsonl (the claim's own rule). A narration record is one transcript record carrying at least one thinking block whose signature decodes, via protobuf field path 2 -> 1 -> 8 taking the LAST length-delimited field at each level, to the exact string 'narration'. csift search --count-by label counts one per record per leaf; csift stats and the independent decoder count one per BLOCK - the two agree here at 24 because every narration record in this set carries exactly one narration block. The subagent figure is derived as (spanned count) - (--no-subagents count) and cross-checked directly by counting hits whose is_subagent field is true; both give 24.",
"note": "REFUTED, and the refutation is triple-instrumented. Claude Code does write narration-tagged thinking blocks into subagent transcripts: 24 of them sit in 5 subagent transcripts of the live corpus, and the spanned-versus-restricted census that the claim cites as proof of equality now differs by exactly that 24 (3103 vs 3079). The blocks are genuine, not a csift artifact - a protobuf decoder written independently of csift finds the same 24 signatures tagged 'narration' in the same five files, and a raw byte grep for the three base64 alignments of the tag returns the same 2/2/6/1/13 per-file split.\n\nNew behavior as measured: narration in subagent lanes is real but sporadic and bursty. It is concentrated - all 5 carrying transcripts belong to ONE parent session, while 10 sibling subagent transcripts of that same session, running the same single model id, carry only 'thinking'-tagged signatures. So it is not gated on the subagent lane and not gated on the model; within one session some subagent turns get a narration block and others do not. Both on-disk shapes are represented: four bare built-in subagent transcripts and one name-embedded teammate transcript. Base rates for a stranger: 24 narration records across ~7.5k subagent transcripts (5 transcripts, 0.07% of files) versus 3079 across 66 top-level transcripts (22 transcripts, 33% of files) - three orders of magnitude rarer per file, which is a plausible reason the original scan read as a clean zero.\n\nHonesty about WHY it drifted: every one of the 24 records carries client version 2.1.251, not 2.1.258, and all five files were fully written on 2026-09-01 - the same date the claim records as its measurement. So these records were already on disk when the claim was made, and this is most likely a scope gap in the original measurement rather than a behavior change between 2.1.251 and 2.1.258. Either reading leaves the claim's assertion false against the current corpus, which is why the verdict is drifted rather than refined: the claim's entire content is an absence, and the absence does not hold.\n\nWhat survives intact is the claim's 'depends' clause, and it is now positively demonstrated rather than merely argued: csift runs the identical signature classifier on subagent files, so the subagent narration was counted with no code change - src/stats.rs:289-291 is unmoved and its counter reports 30 blocks spanned vs 6 restricted on the affected session. The ledger entry should be rewritten from an absence claim into a rarity claim with the per-file base rates above."
}
]
},
{
"id": "PATH-001",
"area": "path-encoding",
"behavior": "Claude Code derives the `<ENCODED>` project-directory basename from the session's canonical start cwd by NFC-normalizing it and replacing every UTF-16 CODE UNIT outside [A-Za-z0-9] with exactly one `-`, position-preserving: no collapsing of consecutive dashes and no case folding, so `.`, `/`, `_` and space each become one dash, an NFD-spelled accent recomposes to a single unit (one dash), and an astral character (two surrogate units) yields TWO dashes. Since 2.1.234 this is the DEFAULT rather than the only path: the basename is `CLAUDE_CODE_PROJECT_DIR_NAME ?? sanitize(cwd)`, and the override is honored only when `CLAUDE_CONFIG_DIR` is also set and the value matches /^[A-Za-z0-9_-]{1,64}$/ and is not a Windows reserved device name.",
"depends": "`path::encode_cwd` reproduces the transform unit-wise to locate the project directory; a char-wise or byte-wise replacement diverges on any non-ASCII cwd, so every subcommand that takes a real path or `.` as its target silently scans the wrong project or none.",
"code": [
{
"path": "src/path/home.rs",
"lines": "5-13",
"snippet": "/// Encode an absolute cwd to its Claude Code project-dir basename - the EXACT transform\n/// CC applies (extracted verbatim from the 2.1.228 binary): the cwd is first\n/// NFC-normalized (every CC path rides `.normalize(\"NFC\")` - a macOS filesystem hands out\n/// NFD, so an accented path is re-composed before encoding), then the JS regex\n/// `replace(/[^a-zA-Z0-9]/g,\"-\")` runs per UTF-16 CODE UNIT: every unit outside ASCII\n/// alphanumerics becomes ONE `-` - so an astral char (two surrogate units) yields TWO\n/// dashes, and a Windows `C:\\Users\\x` yields `C--Users-x` (`:` and `\\` are one unit\n/// each). No dash collapsing, no case folding. A char-wise or byte-wise replacement\n/// DIVERGES from CC on any non-ASCII cwd and resolves the wrong dir."
},
{
"path": "src/path/home.rs",
"lines": "15-27",
"snippet": "pub fn encode_cwd(cwd: &Path) -> String {\n use unicode_normalization::UnicodeNormalization;\n let s = cwd.to_string_lossy();\n let s: String = s.nfc().collect();\n let mut out = String::with_capacity(s.len());\n for unit in s.encode_utf16() {\n match u8::try_from(unit) {\n Ok(b) if b.is_ascii_alphanumeric() => out.push(char::from(b)),\n _ => out.push('-'),\n }\n }\n out\n}"
},
{
"path": "src/path/tests/encode.rs",
"lines": "97-103",
"snippet": " // An astral char is TWO surrogate units → TWO dashes (the JS regex's view).\n assert_eq!(encode_cwd(Path::new(\"/tmp/x\\u{1D11E}y\")), \"-tmp-x--y\");\n // A Windows cwd: the drive colon and each backslash are one dash → `C--…`.\n assert_eq!(\n encode_cwd(Path::new(r\"C:\\Users\\dev\\proj\")),\n \"C--Users-dev-proj\"\n );"
}
],
"instrument": "Pick any directory under `~/.claude/projects/`, read the `cwd` of the first record of one contained `*.jsonl` (`head -1 <file> | jq -r .cwd`) and apply the transform by hand: the result must equal the directory basename character for character. Counting rule: one output dash per UTF-16 code unit that is not ASCII-alphanumeric, so an astral character contributes two dashes and an NFC-composed accent one. The unit test `encode_matches_cc_exactly_on_non_ascii_and_windows_paths` pins NFC/NFD parity, the astral double dash and the Windows shape; to re-derive from the harness, run `strings` over the shipped Claude Code binary and grep for the literal replacement regex `[^a-zA-Z0-9]` and for `normalize(\"NFC\")`, expecting one sanitizer and no case fold.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.3",
"source": "SPEC.md section 2.1; SPEC.md section 6 v0.7.3 ledger; AGENTS.md section 3.1; CHANGELOG 0.7.3; src/path/home.rs encode_cwd doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{500}function KA\\(e\\)\\{let n=k\\(e\\).{200}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{60}CLAUDE_CODE_PROJECT_DIR_NAME.{700}' ; node -e on the extracted regex; python3 pass over ~/.claude/projects comparing each directory basename against the encoding of the first cwd found in its top-level transcripts",
"observed": "Binary carries the sanitizer verbatim: `function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")}` and `function KA(e){let n=k(e);if(n.length<=IL)return n;return `${n.slice(0,IL)}-${be(e)}`}`. But the project key is now `function Em(e){return Apr()??KA(e)}` with `var Apr=Zo(()=>s()?sLn(_()):void 0,I)`, `function _(){return process.env.CLAUDE_CODE_PROJECT_DIR_NAME}`, `function s(){return process.env.CLAUDE_CONFIG_DIR}`, validator `D=/^[A-Za-z0-9_-]{1,64}$/` plus reserved-name reject `C=/^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/i`. The binary's own changelog text reads: '## 2.1.234 - Added the optional `CLAUDE_CODE_PROJECT_DIR_NAME` environment variable: hosts that give each session its own config directory can choose a short name for the per-project transcript directory'. Executing the extracted regex: '/tmp/x\\u{1D11E}y' => '-tmp-x--y' (astral char = TWO dashes); NFD '/tmp/cafe\\u0301' => '-tmp-cafe-' (10 chars) vs NFC '/tmp/caf\\u00e9' => '-tmp-caf-' (9 chars), equal only after .normalize('NFC'). Live corpus: 12 of 12 project directories holding top-level transcripts had at least one transcript whose first recorded cwd encodes byte-for-byte to the basename; 0 exceptions.",
"rule": "One output dash per UTF-16 code unit outside [A-Za-z0-9], position-preserving (no collapsing, no case fold). A directory counts as confirming iff some top-level transcript's first recorded cwd, NFC-normalized and transformed unit-wise, equals the basename character for character. Denominator = project directories holding at least one top-level *.jsonl.",
"note": "The transform itself is unchanged and confirmed both in the binary and by executing the extracted regex. The refinement is an added escape hatch introduced in 2.1.234, after this claim's 2.1.228 baseline: when a host sets CLAUDE_CONFIG_DIR, it may also set CLAUDE_CODE_PROJECT_DIR_NAME to a short literal basename, and the cwd is then not encoded at all. csift's encode_cwd has no counterpart, so under both env vars every path-taking target would resolve to a directory that does not exist while the real transcripts sit under the literal name. Not reachable on this machine (neither variable is set here); the gap is a resolution miss, not a wrong-directory read, because resolution is fail-closed."
}
]
},
{
"id": "PATH-002",
"area": "path-encoding",
"behavior": "Claude Code encodes the CANONICAL form of the cwd - symlinks resolved, then Unicode-NFC - rather than the raw string it was given, so two spellings of the same directory (a symlinked path and its real path, an NFD and an NFC spelling) converge on ONE projects directory.",
"depends": "csift's `absolutize` canonicalizes and `encode_cwd` applies NFC before encoding; without both, a symlinked or NFD-spelled target argument encodes to a directory name that does not exist on disk and resolution fails. csift canonicalizes realpath only, so a non-ASCII NFC-vs-NFD mismatch between a STORED `cwd` string and the target is the one accepted comparison edge.",
"code": [
{
"path": "src/path/home.rs",
"lines": "6-8",
"snippet": "/// CC applies (extracted verbatim from the 2.1.228 binary): the cwd is first\n/// NFC-normalized (every CC path rides `.normalize(\"NFC\")` - a macOS filesystem hands out\n/// NFD, so an accented path is re-composed before encoding), then the JS regex"
},
{
"path": "src/path/home.rs",
"lines": "182-189",
"snippet": "/// Absolutize a real filesystem path WITHOUT requiring it to exist (the project\n/// the cwd points at may have been deleted while its transcripts remain). We\n/// canonicalize when possible to resolve symlinks/`..`, else fall back to\n/// joining with the current dir + lexical normalization.\npub fn absolutize(p: &Path) -> Result<PathBuf> {\n if let Ok(c) = std::fs::canonicalize(p) {\n return Ok(c);\n }"
},
{
"path": "src/path/project_dirs.rs",
"lines": "138-141",
"snippet": "/// Whether a stored `cwd` string denotes the same directory as `want` (the canonical\n/// target). Trailing-slash tolerant. Exact for the ASCII paths that dominate; a non-ASCII\n/// NFC-vs-NFD mismatch is the one accepted edge (CC stores realpath+NFC, csift realpath only).\npub(crate) fn cwd_equivalent(stored: &str, want: &Path) -> bool {"
}
],
"instrument": "On a filesystem that hands out NFD (macOS), create a directory whose name carries a decomposed accent, start a session in it, then compare a listing of `~/.claude/projects` against `csift list <that path>` given each spelling: exactly one directory must exist and both spellings must resolve to it. Counting rule: one directory per distinct canonical cwd.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.3",
"source": "SPEC.md section 2.1; src/path/home.rs absolutize doc; src/path/project_dirs.rs cwd_equivalent doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'import\\{[^}]{0,120}\\}from\"fs\";import\\{cwd as c\\}from\"process\";function o\\(n\\)\\{return n\\.normalize\\(\"NFC\"\\)\\}function uLn' ; csift --claude-home $FIX list <real dir> vs list <symlink to it> --no-subagents --format json",
"observed": "Binary: `import{realpathSync as r}from\"fs\";import{cwd as c}from\"process\";function o(n){return n.normalize(\"NFC\")}function uLn(){let n=\"\";if(typeof process<\"u\"&&typeof process.cwd===\"function\"&&typeof r===\"function\")try{let e=c();try{n=o(r(e))}catch{n=o(e)}}catch{}return n}` followed by `var Lpr=uLn()` and `function dn(e=uLn()){return v7t({...project:{originalCwd:e,projectRoot:e,cwd:e}})}`. So the recorded start cwd is NFC(realpathSync(process.cwd())), with NFC(process.cwd()) as the throw-fallback. Fixture: the symlink spelling and the real spelling encode to DIFFERENT strings (equality check printed False), yet `csift list` returned 1 session row for each, from the one directory named after the real path's encoding. Executing the extracted regex, an NFD and an NFC spelling of the same accented directory converge to the identical basename after .normalize('NFC') (both '-tmp-caf-').",
"rule": "One project directory per distinct canonical cwd. Both spellings of one directory must return the same non-zero row count and resolve to the same on-disk basename; row count = lines whose envelope kind is 'session'.",
"note": "Both halves confirmed: symlinks are resolved by realpathSync and the result is NFC-normalized before it is ever encoded, so a symlinked spelling and an NFD spelling converge on one directory. The one nuance worth carrying: realpath resolution is best-effort - if realpathSync throws, Claude Code falls back to the raw process.cwd() under NFC only, so a cwd that cannot be resolved is stored un-canonicalized. The live corpus cannot discriminate on its own (27 of 27 still-existing recorded cwd values already equal their realpath and 31 of 31 are already NFC, with zero non-ASCII and zero symlinked cwds), which is why the binary and the fixture carry this verdict."
}
]
},
{
"id": "PATH-003",
"area": "path-encoding",
"behavior": "Because the sanitizer does no slash, colon or case pre-processing, a Windows cwd encodes to a LETTER-led directory name - `C:\\Users\\x` becomes `C--Users-x` (the drive letter passes through, the `:` and each `\\` are one unit each) - while a UNC root `\\\\server\\share` becomes the double-dash-led `--server-share`; a Unix cwd is always single-dash-led.",
"depends": "csift's `@`-token grammar admits both encoded shapes: a leading `-` token and the `<letter>--...` drive shape are project-dir tokens (a drive-shaped token that matches no directory falls through to real-path resolution, since it can also be a relative path), and a UNC-encoded directory is targeted as `@--server-...` because a bare `--`-leading token is reserved for the mistyped-flag guard; without the drive-shape arm a Windows project directory is unreachable.",
"code": [
{
"path": "src/path/home.rs",
"lines": "135-145",
"snippet": "/// True iff `token` is a plausible pre-encoded projects-dir basename, per §2.3 step 1:\n/// only `[A-Za-z0-9-]` (so no `/`), and one of the two shapes CC's encoder can emit for\n/// an absolute path - a Unix cwd leads with `-` (the leading `/` encodes to `-`; a UNC\n/// `\\\\server\\…` leads with `--`), a WINDOWS cwd leads with `<drive-letter>--` (the `:`\n/// and `\\` of `C:\\` each encode to `-`, verbatim from the 2.1.228 binary's sanitizer).\npub(crate) fn looks_like_encoded_token(token: &str) -> bool {\n if !token.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {\n return false;\n }\n token.starts_with('-') || is_drive_encoded_token(token)\n}"
},
{
"path": "src/path/home.rs",
"lines": "147-154",
"snippet": "/// The Windows drive-letter encoded shape: `<letter>--…` (`C:\\Users\\x` → `C--Users-x`).\n/// Distinct from every other token class: a Unix encoded dir leads with `-`, an id is\n/// hex/uuid-shaped, and a real RELATIVE path named like this is disambiguated by the\n/// caller (encoded-dir lookup first, real-path fallthrough on a miss).\npub(crate) fn is_drive_encoded_token(token: &str) -> bool {\n let b = token.as_bytes();\n b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b'-' && b[2] == b'-'\n}"
},
{
"path": "src/path/tests/encode.rs",
"lines": "40-113",
"snippet": "#[test]\nfn encoded_token_shapes_unix_windows_unc() {\n assert!(looks_like_encoded_token(\"-Users-dev-example-project\"));\n assert!(looks_like_encoded_token(\"C--Users-dev-proj\")); // Windows drive shape\n assert!(looks_like_encoded_token(\"--server-share-proj\")); // UNC (leads with --)\n assert!(!looks_like_encoded_token(\"Users-dev\")); // no encoded lead-in\n assert!(!looks_like_encoded_token(\"C--Users/dev\")); // a separator disqualifies\n assert!(!looks_like_encoded_token(\"C-Users-dev\")); // one dash after the drive ≠ `C:\\`"
}
],
"instrument": "The unit test `encoded_token_shapes_unix_windows_unc` pins all three lead-in shapes, and `encode_matches_cc_exactly_on_non_ascii_and_windows_paths` pins the drive encoding itself; end-to-end, `csift list '@C--Users-x'` against a fixture tree built with `--claude-home` must resolve the token as an encoded project dir rather than reject it as a mistyped flag. Counting rule: one dash per non-alphanumeric UTF-16 unit, so `C:\\` yields `C--`. Standing caveat: a scan of the development corpus found ZERO drive-letter-led project directories, so the on-disk shape is derived from the sanitizer, not observed - only a real Windows session settles it.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.3",
"source": "SPEC.md section 2.1; SPEC.md section 6 v0.7.3 ledger; AGENTS.md section 3.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "node on the sanitizer extracted verbatim from the binary (`const k=(e)=>e.replace(/[^a-zA-Z0-9]/g,\"-\")`) applied to a Windows drive path, a UNC root and a Unix path; csift --claude-home $FIX list '@C--Users-x' / '@--server-share-proj' / bare '--server-share-proj'; ls ~/.claude/projects | awk '{print substr($0,1,1)}' | sort | uniq -c",
"observed": "Sanitizer output: 'C:\\\\Users\\\\x' => 'C--Users-x' (len 10, letter-led); '\\\\\\\\server\\\\share\\\\proj' => '--server-share-proj' (len 19, double-dash-led); '/Users/dev/proj' => '-Users-dev-proj' (single-dash-led). The binary applies KA/k with no platform branch (the only platform switch nearby belongs to the cache-directory helper, not to the projects key). csift: '@C--Users-x' -> 1 session row; '@--server-share-proj' -> 1 session row; bare unprefixed 'C--Users-x' -> 1 session row; bare '--server-share-proj' -> clap usage error (mistyped-flag guard). Live corpus: 15 of 15 project directory basenames lead with '-'; 0 drive-shaped (letter followed by '--') and 0 UNC-shaped.",
"rule": "One dash per non-alphanumeric UTF-16 code unit, so 'C:\\\\' yields 'C--'. Lead-in class counted as the first character of each basename under ~/.claude/projects. csift row count = envelope lines whose kind is 'session'.",
"note": "The encoding shapes are no longer derived-only: running Claude Code's own extracted sanitizer produces the letter-led drive form and the double-dash-led UNC form directly, and csift admits both token shapes while still rejecting a bare '--'-leading token as a mistyped flag. The ledger's standing caveat survives unchanged for the on-disk half: this corpus still contains zero drive-letter-led project directories (15/15 lead with '-'), so the claim that a real Windows session actually lands in such a directory is confirmed only through the sanitizer, not by observation. A single Claude Code session run on Windows, then listing the basenames under its projects root, would settle it."
}
]
},
{
"id": "PATH-004",
"area": "path-encoding",
"behavior": "Claude Code caps the encoded project-directory basename at 200 characters (`MAX_SANITIZED_LENGTH`) and stores anything longer as `<first-200>-<hash>`, so for a deeply-nested project the full encoding of the cwd does not exist on disk at all.",
"depends": "csift compares the encoded length against the same 200-character cap before falling back to the prefix scan; a wrong cap leaves every deeply-nested project unresolvable by `csift list .` and by every other path-taking target.",
"code": [
{
"path": "src/path/project_dirs.rs",
"lines": "73",
"snippet": "pub(crate) const MAX_SANITIZED_LENGTH: usize = 200;"
},
{
"path": "src/path/project_dirs.rs",
"lines": "52-53",
"snippet": " if encoded.len() > MAX_SANITIZED_LENGTH {\n let prefix = format!(\"{}-\", &encoded[..MAX_SANITIZED_LENGTH]);"
}
],
"instrument": "List the basenames under `~/.claude/projects` with their lengths (`ls ~/.claude/projects | awk '{ print length($0), $0 }' | sort -rn | head`): any basename longer than 200 must be exactly 200 encoded characters, then a `-`, then a hash token. Counting rule: characters of the directory basename, not bytes of the cwd.",
"located": {
"claude_code": "2.1.228",
"csift": "0.2.0",
"source": "SPEC.md section 2.1; AGENTS.md section 3.1; src/path/project_dirs.rs MAX_SANITIZED_LENGTH"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{100}MAX_SANITIZED_LENGTH.{100}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{500}function KA\\(e\\)\\{let n=k\\(e\\).{200}' ; ls ~/.claude/projects | awk '{print length($0)}' | sort -rn | head -1 ; fixture: build a directory whose encoding exceeds 200 chars, mint the directory name with the binary's own formula, then csift --claude-home $FIX list <that path>",
"observed": "Binary exports the constant: `IL as MAX_SANITIZED_LENGTH` and defines `var IL=200` immediately before `function be(e){return Math.abs(zq(e)).toString(36)}function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")}function KA(e){let n=k(e);if(n.length<=IL)return n;return `${n.slice(0,IL)}-${be(e)}`}`. A second, independent sanitizer for the cache directory carries the same literal: `var v=m(\"claude-cli\"),C=200`. Live corpus: longest project-directory basename is 117 characters; 0 basenames exceed 200, so the cap is not exercised here. Fixture: a cwd encoding to 236 characters produced a key of 207 = 200-character prefix + '-' + 6-character suffix, the full 236-character encoding did not exist on disk, and csift resolved the real path to the capped directory (1 session row).",
"rule": "Characters of the directory basename, not bytes of the cwd. A capped name must be exactly 200 encoded characters, then one '-', then a non-empty suffix.",
"note": "The 200 cap holds and is now pinned to a named export rather than an inferred symbol. Worth recording that the corpus cannot exercise it: the deepest project here encodes to 117 characters, so csift's long-path branch is reached only under a synthetic fixture on this machine. code-site note: src/path/project_dirs.rs:72 dates the cap to a sanitizer symbol that is not present in 2.1.258; in 2.1.258 the cap is `var IL=200`, re-exported as `IL as MAX_SANITIZED_LENGTH`, and the sanitizer is the pair `k` (the regex) and `KA` (the cap plus hash suffix). The value 200 is unchanged; only the symbol name and verification date in that comment are stale."
}
]
},
{
"id": "PATH-005",
"area": "path-encoding",
"behavior": "For a cwd whose encoding exceeds 200 characters Claude Code stores the directory as `<first-200>-<hash>`, and its own project-directory lookup PREFIX-SCANS the projects root for `<first-200>-` rather than recomputing the hash to address the directory. In 2.1.258 that suffix is a single deterministic function of the original cwd string - a 31-multiplier int32 hash, absolute-valued, base-36 - so it IS reconstructible by a reader who holds the binary and the exact cwd; the reason to prefix-scan is that the harness itself prefix-scans and that the suffix input is the pre-sanitized cwd, not the recoverable directory name.",
"depends": "csift mirrors the prefix scan (`find_dir_by_prefix`) and, among multiple matches, prefers the directory whose first session's recorded `cwd` equals the target, falling back to the sole or first match; recomputing a digest instead would resolve nothing for a deeply-nested project.",
"code": [
{
"path": "src/path/project_dirs.rs",
"lines": "75-80",
"snippet": "/// Resolve a >200-char encoded path to its on-disk dir by prefix-scanning the projects\n/// root for `<first-200>-<hash>` (the hash is not reconstructible - see [`resolve_target`]).\n/// Among multiple matches (two paths identical for the first 200 encoded chars - vanishingly\n/// rare), prefer the dir whose first session's recorded `cwd` equals the target; otherwise\n/// fall back to the sole / first match. Returns `None` when nothing matches.\npub(crate) fn find_dir_by_prefix(root: &Path, prefix: &str, abs: &Path) -> Result<Option<PathBuf>> {"
},
{
"path": "src/path/project_dirs.rs",
"lines": "52-59",
"snippet": " if encoded.len() > MAX_SANITIZED_LENGTH {\n let prefix = format!(\"{}-\", &encoded[..MAX_SANITIZED_LENGTH]);\n if let Some(found) = find_dir_by_prefix(&root, &prefix, &abs)? {\n return Ok(ProjectDir {\n dir: found,\n target_cwd: Some(abs.clone()),\n });\n }"
}
],
"instrument": "Create a directory whose absolute path encodes to more than 200 characters, open a session in it, then run `csift list <that path>`: it must return rows even though the directory named by the full encoding does not exist. Counting rule: one resolved directory per target; with more than one prefix match, disambiguation is by the in-record `cwd` of each candidate's first session.",
"located": {
"claude_code": "2.1.228",
"csift": "0.2.0",
"source": "SPEC.md section 2.1; AGENTS.md section 3.1; src/path/project_dirs.rs find_dir_by_prefix doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{500}function KA\\(e\\)\\{let n=k\\(e\\).{200}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{200}Ctt\\(.{200}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{150}Bun\\.hash.{150}' ; fixture: reimplement the binary's suffix formula in python, mint the directory, then csift --claude-home $FIX list <deep path>",
"observed": "The suffix formula is fully present in the binary: `function zq(t){let e=0;for(let r=0;r<t.length;r++)e=(e<<5)-e+t.charCodeAt(r)|0;return e}` and `function be(e){return Math.abs(zq(e)).toString(36)}`, consumed by `KA` as `${n.slice(0,IL)}-${be(e)}`. That is a 31-multiplier int32 hash over the ORIGINAL cwd string, absolute-valued and base-36 encoded - deterministic and reconstructible. `Bun.hash` does occur in the binary but in unrelated paths (skill content hashes, message hashing, and an xxHash64 backup-file namer); it does not appear in KA. Reconstructing be() from these bytes produced a 6-character suffix that csift's prefix scan then resolved (1 session row). The prefix scan itself is confirmed on the harness side: `let i=KA(e);if(i.length<=IL)return t;let o=ia(),...,s=u(i.slice(0,IL)+\"-\"),p=u(r);try{for(let l of <readdir>){if(!l.isDirectory()||!u(l.name).startsWith(s))continue;...}}` - Claude Code enumerates the projects root and keeps names starting with the 200-character prefix plus '-' rather than recomputing the hash to address the directory.",
"rule": "One resolved directory per target. The suffix is 'reconstructible' iff a reader holding only the binary and the exact cwd string can compute the on-disk basename; tested by minting the directory from the reconstructed formula and requiring the consumer's prefix scan to return a non-zero row count.",
"note": "The behavior csift depends on is intact and was confirmed twice over: the harness prefix-scans, and csift's mirror of it resolves a capped directory the full encoding does not name. What failed refutation-testing is the stated reason. Keeping the prefix scan is still correct - it is what the harness does, it costs one readdir, and it stays right if a second producer ever writes a different suffix - but the code comment should no longer claim the suffix is uncomputable, because this verification computed it. code-site note: src/path/project_dirs.rs:49-51 states 'The suffix is NOT reconstructible (the CLI uses Bun.hash, the SDK djb2 - different digests for the same path)'. Against 2.1.258 that is inaccurate on both counts: there is one digest in the shipped project-key path and it is not Bun.hash. The operative sentence that follows - that Claude Code prefix-scans rather than recomputing, and that csift mirrors it - is confirmed and should be kept as the whole justification."
}
]
},
{
"id": "PATH-006",
"area": "path-encoding",
"behavior": "Claude Code resolves its config home as `CLAUDE_CONFIG_DIR` when that variable is set, else the OS home directory joined with `.claude`, and NFC-normalizes the result; when the variable is set, every path that would be `~/.claude/...` - the `projects/` tree included - lives under that directory instead.",
"depends": "csift resolves its data root with the precedence `--claude-home` flag > `$CLAUDE_CONFIG_DIR` (when non-empty) > the OS home's `.claude`, and every subcommand reaches the data through that one resolver; ignoring the variable would make csift read a different corpus than the harness writes.",
"code": [
{
"path": "src/path/home.rs",
"lines": "60-63",
"snippet": "/// Claude Code's own config-dir relocation env var. When set, \"every `~/.claude` path\n/// lives under that directory instead\", so csift - which reads Claude Code's data - must\n/// honor it to keep pointing at the same files.\npub const CLAUDE_CONFIG_DIR_ENV: &str = \"CLAUDE_CONFIG_DIR\";"
},
{
"path": "src/path/home.rs",
"lines": "72-75",
"snippet": "/// Pure precedence resolver for the Claude config dir, factored out so the ordering is\n/// unit-testable without touching process-global env / `OnceLock` state. Order:\n/// 1. explicit `--claude-home` flag, 2. `$CLAUDE_CONFIG_DIR` (when non-empty),\n/// 3. `$HOME/.claude`."
},
{
"path": "src/path/home.rs",
"lines": "84-90",
"snippet": " if let Some(d) = config_dir_env {\n if !d.is_empty() {\n return PathBuf::from(d);\n }\n }\n home.join(\".claude\")\n}"
}
],
"instrument": "Point `CLAUDE_CONFIG_DIR` at a fixture tree holding `projects/<ENCODED>/<uuid>.jsonl` and run `csift list`: only the fixture rows may appear. The pure resolver `resolve_claude_home(flag, env, home)` is unit-testable without touching the process-global override, and the e2e case `custom_claude_home_via_env_var_and_flag` exercises the flag before, after and instead of the env var. To re-derive the harness side, run `strings` over the shipped Claude Code binary and grep for `CLAUDE_CONFIG_DIR`. Counting rule: one config-home resolver reached by every subcommand.",
"located": {
"claude_code": "2.1.228",
"csift": "0.2.0",
"source": "SPEC.md section 2.1; SPEC.md section 6 v0.7.3 ledger; AGENTS.md section 7; src/path/home.rs CLAUDE_CONFIG_DIR_ENV doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{250}\"projects\".{250}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'import\\{[^}]*homedir[^}]*\\}from\"os\".{0,80}' ; env -u CLAUDE_CONFIG_DIR HOME=$FIX/home csift list --no-subagents --format json ; env HOME=$FIX/home CLAUDE_CONFIG_DIR=$FIX/alt csift list ... ; env HOME=$FIX/home CLAUDE_CONFIG_DIR=$FIX/alt csift --claude-home $FIX/home/.claude list ...",
"observed": "Binary: `function s(){return process.env.CLAUDE_CONFIG_DIR}var Se=Zo(()=>(s()??i(R(),\".claude\")).normalize(\"NFC\"),s)` with `import{homedir as R}from\"os\"` in the same chunk, and the projects tree hangs off it as `function ia(){return S(Se(),\"projects\")}`. Every other namespace in the path builder is likewise rooted at that configHome (transcripts, sidecars, tasks, sessions, file-history). csift precedence, three runs against two fixture trees: HOME only -> 1 row, the home-tree session; HOME plus CLAUDE_CONFIG_DIR -> 1 row and the recorded cwd printed belongs to the alt tree only; --claude-home plus CLAUDE_CONFIG_DIR -> the home-tree cwd only.",
"rule": "One config-home resolver reached by every subcommand. Each run must surface sessions from exactly one fixture tree; identified by the distinct cwd marker string each tree's single session carries.",
"note": "Confirmed on both sides, including the detail that the harness NFC-normalizes the resolved config home itself, not just the paths beneath it. csift's documented divergence on an EMPTY value is untouched by this claim and is the separate PATH-011 question; the binary's `??` here is the same coalescing operator that claim turns on, so an exported-but-empty variable would resolve the harness to the empty root."
}
]
},
{
"id": "PATH-007",
"area": "path-encoding",
"behavior": "Claude Code does not implement a per-platform home branch itself: it resolves `.claude` as join(os.homedir(), '.claude') and NFC-normalizes the result, delegating the platform split to the runtime's os.homedir(), which reads $HOME on Unix and %USERPROFILE% on Windows. The consequence the claim rests on is unchanged - nothing in the harness's config-home path consults HOME on Windows, so a Git-Bash or MSYS shell's exported POSIX-style HOME cannot steer it.",
"depends": "csift's `home_dir()` is cfg-split the same way, so a stray POSIX `HOME` inside a Windows shell does not point csift at a `.claude` directory Claude Code never writes; the conventional variable is read first so a test harness can relocate home per subprocess.",
"code": [
{
"path": "src/path/home.rs",
"lines": "31-35",
"snippet": "/// load-bearing on Windows - CC never consults `HOME` there, but Git-Bash/MSYS shells\n/// export one (often a POSIX-style `/c/Users/...` a native process cannot use), and\n/// honoring it would point csift at a `.claude` dir CC never writes. The conventional env\n/// var is read first so a test harness can relocate home per-subprocess; `std::env::home_dir`\n/// (un-deprecated, Windows-correct since Rust 1.85 - MSRV is above both) is the fallback."
},
{
"path": "src/path/home.rs",
"lines": "36-48",
"snippet": "pub(crate) fn home_dir() -> Result<PathBuf> {\n #[cfg(not(windows))]\n if let Some(h) = std::env::var_os(\"HOME\") {\n if !h.is_empty() {\n return Ok(PathBuf::from(h));\n }\n }\n #[cfg(windows)]\n if let Some(h) = std::env::var_os(\"USERPROFILE\") {\n if !h.is_empty() {\n return Ok(PathBuf::from(h));\n }\n }"
}
],
"instrument": "On Windows, run `csift list` from a Git-Bash shell that exports a POSIX-style `HOME` and confirm the rows still come from the profile directory's `.claude\\projects`; print both `HOME` and `%USERPROFILE%` in that shell and compare against where `projects/` actually exists. On Unix the equivalent check is the e2e case `custom_claude_home_via_env_var_and_flag`, which spawns the binary with `HOME` set to a temp fixture. Counting rule: one home resolution per process.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.1",
"source": "SPEC.md section 2.1; SPEC.md section 6 v0.7.3 ledger; AGENTS.md section 7; CHANGELOG 0.7.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'import\\{[^}]*homedir[^}]*\\}from\"os\".{0,80}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{200}USERPROFILE.{200}' ; env -u CLAUDE_CONFIG_DIR HOME=$FIX/home csift list --no-subagents --format json",
"observed": "Claude Code does not branch on platform in this path at all: the config home is `(process.env.CLAUDE_CONFIG_DIR ?? i(R(),\".claude\")).normalize(\"NFC\")` where R is `import{homedir as R}from\"os\"`, so the per-platform split lives inside the runtime's os.homedir() rather than in harness code. Consistently, the 26 USERPROFILE occurrences in the binary sit in unrelated features - IDE discovery, a PowerShell executable probe, desktop-path resolution, a separate ANTHROPIC_CONFIG_DIR resolver - and none of them participate in resolving `.claude`. No HOME branch appears in the config-home path either. csift Unix half: with CLAUDE_CONFIG_DIR unset and HOME pointed at a fixture tree, csift listed 1 session row, the fixture's own.",
"rule": "One home resolution per process; the run must surface exactly the fixture tree's session count and no others. Occurrence counting for USERPROFILE is literal matches in the extracted strings, classified by the enclosing function.",
"note": "The Unix half is verified here and the 'no HOME branch on the Windows path' half is supported by the binary, which names HOME nowhere in the config-home resolution. What this machine cannot decide is the last link: that os.homedir() under the runtime Claude Code actually ships reads %USERPROFILE% and ignores a POSIX HOME on Windows. That is a property of the runtime, not of the harness. Deciding it needs a Windows session: run Claude Code from a Git-Bash shell exporting a POSIX-style HOME, print HOME and %USERPROFILE%, and check which of the two directories gained a .claude\\projects tree. This corpus offers no proxy - 15 of 15 project directories are Unix-shaped."
}
]
},
{
"id": "PATH-008",
"area": "path-encoding",
"behavior": "The cwd-to-directory map is many-to-one and Claude Code does not disambiguate collisions for any cwd of 200 encoded characters or fewer: two different cwds that encode identically share ONE projects directory, distinguishable only by the session uuid and by each record's own `cwd` field.",
"depends": "csift uses the encoded directory as the lookup key exactly as the harness does, and intends to keep, for a REAL-path target, only the files whose recorded cwd IS that path while an EXPLICIT encoded-dir token skips the filter. As of Claude Code 2.1.258 that filter no longer fires on real data: the harness now writes a session-state cache line as physical line 1 of every transcript, and read_first_cwd inspects only line 1, so the stored cwd reads as absent and every candidate is admitted under the absent-cwd keep. The mechanism is intact and testable only where line 1 carries cwd.",
"code": [
{
"path": "src/path/home.rs",
"lines": "126-132",
"snippet": " /// The canonical cwd of a REAL-path target - `Some` when the user passed an actual\n /// filesystem path, `None` for a pre-encoded `<ENCODED>` dir token (where the user\n /// explicitly named the dir) or an all-projects scan. When `Some`, session enumeration\n /// filters this dir's files to those whose recorded `cwd` IS this path, so a lossy-\n /// encoding COLLISION (a different cwd that encodes to the same dir, §2.1) never leaks a\n /// sibling's sessions - or their subagents - into the result.\n pub target_cwd: Option<PathBuf>,"
},
{
"path": "src/path/resolver.rs",
"lines": "319-324",
"snippet": "/// Admit one candidate `<stem>.jsonl`: the cwd COLLISION GUARD (SPEC 2.1 - a dir resolved\n/// from a REAL path may be shared by a DIFFERENT cwd under the lossy encoding, so keep only\n/// files whose recorded `cwd` IS this target; a file whose `cwd` is absent is kept), then the\n/// UNION-DOMAIN prefix collection (a prefix may name a subagent of a session that itself does\n/// NOT match - cost paid only on a prefix-targeted invocation), then the exact-id / prefix\n/// keep decision."
},
{
"path": "src/path/resolver.rs",
"lines": "341-347",
"snippet": " if let Some(want) = &pd.target_cwd {\n if let Some(stored) = read_first_cwd(&p) {\n if !cwd_equivalent(&stored, want) {\n return;\n }\n }\n }"
}
],
"instrument": "`csift list <ENCODED-dir> --format json | jq -r 'select(.kind==\"session\") | .cwd' | sort -u` - more than one distinct value proves a collision inside that directory, and the same query against the REAL path must return exactly one. Counting rule: distinct `cwd` strings across the directory's top-level session rows.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 2.1; src/path/resolver.rs admit_entry doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 comparing the encodings of two constructed sibling cwds; csift --claude-home $FIX list '@<ENCODED>' vs list <real path A> vs list <real path B> --no-subagents --format json; python3 census of the first physical line of every top-level *.jsonl under ~/.claude/projects; csift list '@<ENCODED>' vs list <real path> against one live project directory",
"observed": "Many-to-one confirmed constructively: two distinct cwds differing only in one separator character encode to the identical 112-character basename (equality check printed True) and share one directory. Guard behaves as documented WHEN the cwd is on line 1: the encoded-dir token returned 2 session rows (no filter, the user named the directory) while each real path returned 1. But the guard is inert against real transcripts: 0 of 66 top-level transcripts in the live corpus carry a cwd field on physical line 1 - line 1 is a session-state cache line in every case (62 'last-prompt', 3 'ai-title', 1 'queue-operation') - and csift's read_first_cwd (src/path/project_dirs.rs:109) reads ONLY the first line, so it returns None and the file is kept unconditionally. Demonstrated on a live directory holding 7 top-level transcripts of which 3 record a cwd that does not encode to the basename: both the encoded-dir token and the real path returned 7 rows; the real path should have returned 4.",
"rule": "A collision is two DISTINCT cwd strings whose encodings are equal - not merely two distinct cwd values in one directory, which the deep-cd case also produces. Guard efficacy = rows returned for a real-path target divided by rows returned for the encoded-dir token over the same directory; 1.0 means the guard never fired. Line-1 census counts every top-level *.jsonl once, keyed on presence of a top-level cwd field in the first physical line.",
"note": "The claim's statement about Claude Code holds - the map is many-to-one and the harness does not disambiguate below the cap. The csift half has drifted out from under it, and this is the most actionable finding in the area. The stated reason for reading only line 1 is a performance one - 'the cwd field sits in the first record's first ~200 bytes' - and that premise is now false for 100% of the corpus. A fix should scan the first few lines, or the first line carrying a message field, rather than exactly one; the bounded 64 KiB head read already covers the extra lines. Note also that the ledger's own instrument for this claim is unsound as written: distinct cwd values inside one directory prove nothing about collisions, because the deep-cd case in PATH-009 produces them too - the encodings must be compared, not the cwd strings."
}
]
},
{
"id": "PATH-009",
"area": "path-encoding",
"behavior": "A projects directory is keyed by the session's START cwd alone: a deep in-session `cd` and a subagent's cwd appear in the record data but never mint a directory of their own, so a directory implies the start cwd of its files while the reverse does not hold - an observed `cwd` value need not have a directory named after its encoding.",
"depends": "csift never enumerates one directory per observed cwd; `list` reads a session's `cwd` from its FIRST record deliberately (the per-record cwd follows the tracked shell cwd, so a last-seen value can legitimately be a subdirectory), and a scoped command that assumed a directory exists for every seen cwd would report empty scopes.",
"code": [
{
"path": "src/session/rows.rs",
"lines": "29-33",
"snippet": " /// Decoded human-readable cwd (read from the data, §2.4), if present. FIRST-seen\n /// deliberately: the record cwd follows the tracked shell cwd (SPEC 4.9), so the\n /// last-seen value can legitimately be a subdirectory; the session's home is the\n /// opening value. Asymmetric with version/git_branch below on purpose.\n pub cwd: Option<String>,"
},
{
"path": "src/session/summarize.rs",
"lines": "28-33",
"snippet": " if let Some(text) = preview_text(rec) {\n // Capture identity off the first user record (it carries cwd / version /\n // gitBranch / sessionId in real data).\n cwd = rec.cwd.clone();\n version = rec.version.clone();\n git_branch = rec.git_branch.clone();"
}
],
"instrument": "Collect the distinct `cwd` values across one directory's top-level transcripts (`rg -o --no-filename '\"cwd\":\"[^\"]+\"' ~/.claude/projects/<ENCODED>/*.jsonl | sort -u`) and compare that count against the number of directories whose basename equals the encoding of each value: the former can exceed the latter. Counting rule: distinct `cwd` string values across one directory's top-level transcripts.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 1; src/session/rows.rs cwd field doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{250}originalCwd:.{150}' ; python3 collecting every distinct cwd value across all top-level transcripts under ~/.claude/projects and testing each against the set of directory basenames",
"observed": "The project directory is built from originalCwd, not from the tracked per-record cwd: `function RS(e){return he(Bu(e.root.project.originalCwd),e.root.id,Bge)}` with `function Bu(e){return S(ia(),Em(e))}`, and originalCwd is seeded once at startup by `function dn(e=uLn()){return v7t({...project:{originalCwd:e,projectRoot:e,cwd:e}})}` while in-session movement updates only the cwd slot: `setCwd(c){S.update({project:{cwd:c}})}`. Live corpus: 31 distinct cwd values observed across top-level transcripts, against 15 project directories; only 13 of the 31 have a directory named after their encoding, and 18 have none.",
"rule": "Distinct cwd string values across one corpus's top-level transcripts, compared against the set of directory basenames under ~/.claude/projects; a value 'has a directory' iff its NFC+unit-wise encoding is exactly some basename. The claim requires observed-cwd count to be able to exceed directory count and requires at least one observed cwd with no directory.",
"note": "Confirmed on both sides, and the binary supplies the mechanism the claim only asserted: the directory is keyed by originalCwd, which is written once at session start and never re-derived, while setCwd moves only the tracked cwd. 18 of 31 observed cwd values have no directory of their own - a comfortable margin, so the asymmetry is not an artifact of a small corpus. csift's choice in src/session/rows.rs to report the FIRST-seen cwd rather than the last is the matching read: first-seen is the value the directory key was derived from."
}
]
},
{
"id": "PATH-010",
"area": "path-encoding",
"behavior": "Some <ENCODED> project directories hold NO top-level *.jsonl at all. On this corpus the observed instance is a directory carrying only a memory/ directory (2 of 15). The variant in which such a directory carries only a nested <session-uuid>/ sidecar tree of subagent transcripts was not observed here and remains asserted rather than measured.",
"depends": "csift's top-level session enumeration is non-recursive and tolerates a jsonl-less (or vanished) project directory by returning an empty result for it; a recursive walk would mis-list a subagent transcript as a session (its bare-hex id is not a re-feedable target) and a hard error on a childless directory would break an all-projects `list`.",
"code": [
{
"path": "src/path/resolver.rs",
"lines": "527-536",
"snippet": "/// The top-level `<uuid>.jsonl` session files directly in `dir` (non-recursive). Tolerates an\n/// unreadable/vanished dir (empty result).\npub(crate) fn top_level_jsonls(dir: &Path) -> Vec<PathBuf> {\n let mut out = Vec::new();\n if let Ok(read) = std::fs::read_dir(dir) {\n for entry in read.flatten() {\n let p = entry.path();\n if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)\n && p.extension().is_some_and(|e| e == \"jsonl\")\n {"
},
{
"path": "src/subagent/ids.rs",
"lines": "24-30",
"snippet": "pub fn session_id_from_path(path: &Path) -> String {\n path.file_stem()\n .and_then(|s| s.to_str())\n .map(bare_agent_id)\n .map(str::to_string)\n .unwrap_or_default()\n}"
}
],
"instrument": "List the project directories holding no top-level jsonl (`for d in ~/.claude/projects/*/; do [ -z \"$(ls \"$d\"*.jsonl 2>/dev/null)\" ] && echo \"$d\"; done`), then run `csift list`: it must complete without error and print no bare-hex session id. Counting rule: one directory per line printed.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 1; src/path/resolver.rs top_level_jsonls doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for d in ~/.claude/projects/*/; do [ -z \"$(ls \"$d\"*.jsonl 2>/dev/null)\" ] && echo \"$d\"; done ; csift list --max-count 0 --format json, then counting session rows and testing each top-level row's session_id against ^[0-9a-f]{12,}$",
"observed": "2 of 15 project directories hold no top-level *.jsonl. Both contain a memory directory and nothing else transcript-shaped (the only other entry seen across the two is a .DS_Store, an operating-system artifact). No directory in this corpus is sidecar-only - that is, none holds a nested <session-uuid>/ subagent tree without also holding top-level transcripts. csift list over the whole corpus exited 0, emitted 7600 session rows of which 7534 are subagent rows, and 0 top-level rows carried a bare-hex session id.",
"rule": "One directory per line printed by the jsonl-less loop; denominator = directories under ~/.claude/projects. csift must exit 0 and emit zero top-level rows whose session_id matches ^[0-9a-f]{12,}$ (the bare-hex shape that is not a re-feedable target).",
"note": "The consequence csift depends on is fully confirmed: the enumeration is non-recursive and tolerant, an all-projects list completes cleanly across 15 directories including the 2 empty ones, and no subagent transcript is ever mis-listed as a session - 0 of 66 top-level rows carry a bare-hex id. Only the enumeration of WHICH empty shapes occur needed narrowing. A sidecar-only directory would arise if every top-level transcript in a project were deleted while its per-session subagent subdirectory survived; deleting the top-level jsonl from a project that has subagents would produce it on demand."
}
]
},
{
"id": "PATH-011",
"area": "path-encoding",
"behavior": "An EMPTY-STRING `CLAUDE_CONFIG_DIR` is still taken as SET by Claude Code, because the null-coalescing operator guarding the read tests only for null/undefined - so the harness resolves its config home to the empty string rather than falling back to the OS home, and every path derived from it becomes RELATIVE to the process cwd (a one-shot run in an empty directory mints `projects/`, `sessions/` and `backups/` there, transcript included). The coalescing read is NOT the only way the harness reads this variable: the global `.claude.json` path and the runner config path read it with a truthiness fallback instead, so an empty value falls back to the OS home on those sites and the harness ends up split between two roots in one process.",
"depends": "csift treats an empty `$CLAUDE_CONFIG_DIR` as UNSET and falls through to the OS home's `.claude` - a deliberate, documented divergence (honoring the empty value would resolve the projects root to the relative path `projects`, i.e. whatever happens to sit under the invoking cwd, and normally a hard `cannot read projects root` error rather than the corpus); the same non-empty test also gates the lazy home lookup, so a relocated root still works when `$HOME` is unset.",
"code": [
{
"path": "src/path/home.rs",
"lines": "72-75",
"snippet": "/// Pure precedence resolver for the Claude config dir, factored out so the ordering is\n/// unit-testable without touching process-global env / `OnceLock` state. Order:\n/// 1. explicit `--claude-home` flag, 2. `$CLAUDE_CONFIG_DIR` (when non-empty),\n/// 3. `$HOME/.claude`."
},
{
"path": "src/path/home.rs",
"lines": "84-88",
"snippet": " if let Some(d) = config_dir_env {\n if !d.is_empty() {\n return PathBuf::from(d);\n }\n }"
},
{
"path": "src/path/home.rs",
"lines": "100-107",
"snippet": " // `$HOME` feeds only the default branch; resolve it lazily so a relocated config dir\n // (flag or env) still works when `$HOME` is unset.\n let have_higher = flag.is_some() || env.as_deref().is_some_and(|d| !d.is_empty());\n let home = if have_higher {\n PathBuf::new()\n } else {\n home_dir()?\n };"
}
],
"instrument": "Harness side: `cd \"$(mktemp -d)\" && CLAUDE_CONFIG_DIR= claude --print 'Reply with exactly: ok'; find . -maxdepth 2` - the run must create `projects/`, `sessions/` and `backups/` in that cwd, and no directory named after that cwd's encoding may appear under ~/.claude/projects. Reading the operator directly: `strings -n 6 <the shipped Claude Code binary> | rg -o '.{0,120}CLAUDE_CONFIG_DIR.{0,220}'` and read the coalescing expression on the config-home resolver (`??`), noting that other read sites of the same variable use a truthiness fallback. csift side: `CLAUDE_CONFIG_DIR= csift list --max-count 1 --format json | head -1` must print the same `sessions_in_scope` as the same command with the variable unset. Counting rule: one config-home resolver per process.",
"located": {
"claude_code": "2.1.228",
"csift": "0.2.0",
"source": "SPEC.md section 2.1; AGENTS.md section 7; src/path/home.rs resolve_claude_home doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}CLAUDE_CONFIG_DIR.{0,220}' (2) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,160}\\(Se\\(\\),\"projects\"\\).{0,160}' (3) cd \"$(mktemp -d)\" && CLAUDE_CONFIG_DIR= claude --print 'Reply with exactly: ok'; find . -maxdepth 2 (4) ls ~/.claude/projects | grep -c <the probe dir's encoded basename> (5) CLAUDE_CONFIG_DIR= csift list --max-count 1 --format json | head -1; env -u CLAUDE_CONFIG_DIR csift list --max-count 1 --format json | head -1; CLAUDE_CONFIG_DIR=/nonexistent-root-xyz csift list --max-count 1 --format json (6) awk 'NR>=72 && NR<=107 { printf \"%d\\t%s\\n\", NR, $0 }' src/path/home.rs",
"observed": "(1) the resolver is present verbatim in 2.1.258: `function s(){return process.env.CLAUDE_CONFIG_DIR}var Se=Zo(()=>(s()??i(R(),\".claude\")).normalize(\"NFC\"),s);` - the guard is `??`, which tests only null/undefined, so an empty string wins the coalesce. (2) the projects root hangs off that same value, in two chunks: `function ia(){return S(Se(),\"projects\")}` and `function Pl(){return o(Se(),\"projects\")}`. (3) one `claude --print` run in an empty scratch directory with CLAUDE_CONFIG_DIR exported EMPTY created 3 top-level directories in the process cwd - `projects/`, `sessions/`, `backups/` - with the transcript at `./projects/<encoded-cwd>/<uuid>.jsonl`; the config home therefore resolved to the empty string and every derived path became RELATIVE to the process cwd. (4) 0 directories matching that cwd's encoding exist under ~/.claude/projects, so nothing fell back to the OS home. (5) csift diverges as documented: empty and unset both report `\"sessions_in_scope\":7597,\"subagent_sessions\":7532,\"top_level_sessions\":65` (byte-identical header lines), while a non-empty bogus value is honored and errors `cannot read projects root /nonexistent-root-xyz/projects`. (6) all three claim snippets are verbatim at the stated lines: 72-75 doc, 84-88 `if let Some(d) = config_dir_env { if !d.is_empty() { return PathBuf::from(d); } }`, 100-107 the lazy-home block.",
"rule": "Harness side: count the top-level directories a single one-shot `claude --print` run creates in an empty scratch cwd while CLAUDE_CONFIG_DIR is exported empty - 3 (projects, sessions, backups) if the empty value is taken as SET, 0 if the harness falls back to the OS home; cross-check with the count of directories under ~/.claude/projects whose basename equals the encoding of that scratch cwd (0 = no fallback). csift side: compare the `kind:\"header\"` line of `csift list --max-count 1 --format json` under an exported-empty vs an unset CLAUDE_CONFIG_DIR - identical `sessions_in_scope` means empty is treated as unset. One config-home resolution per process.",
"note": "Holds at 2.1.258, and now with a live receipt rather than a read of the operator alone: the empty value is taken as set and the config home becomes the empty string, so the harness wrote its whole state tree into the invoking directory. Two refinements. (a) The consequence is a RELATIVE root, not an unreadable one - `join(\"\", \"projects\")` is `projects`, so the harness silently starts a second corpus under whatever cwd it was launched from; the ledger's parenthetical that an empty root 'would make every subcommand read nothing' is close but imprecise. (b) The binary is not uniform about this variable: the config home that mints `projects/` coalesces on null/undefined, while the global `.claude.json` path and the self-hosted-runner config path use a truthiness fallback - observed live, since `CLAUDE_CONFIG_DIR= claude mcp list` in the same empty scratch directory still resolved the user-level server list from the OS home while writing nothing to the cwd. One thing to watch in later versions: 2.1.258 carries the string `the configuration home (CLAUDE_CONFIG_DIR) is not an absolute path`, reached from a scoped placement check (`if(!TI(we)) return vv(..., \"placement\")`), so at least one subsystem already rejects a non-absolute config home; that guard does not sit on the resolution path the probe exercised, but a future version hardening it globally would turn this claim's empty-value branch into an error path. csift's divergence is unaffected either way and is the safer behavior. All three cited code sites are verbatim at their stated lines in the current file, so corrections.code is empty."
}
]
},
{
"id": "PATH-012",
"area": "path-encoding",
"behavior": "Inside a `<ENCODED>` project directory a transcript's filename basename IS the session id: over the whole live corpus, 66 top-level transcripts carried a top-level `sessionId` on at least one record, 0 files carried a `sessionId` that differed from the basename minus `.jsonl`, and 0 files carried more than one distinct `sessionId` value - so the filename and the in-record id are two spellings of one identity, and the id is recoverable without parsing a single line.",
"depends": "csift derives every row's id from the FILENAME and keeps the data-derived `sessionId` only as a fallback for a file whose name yields nothing; the live session-registry join then matches a registry row by its `sessionId` against that filename-derived id, so the two agreeing is what makes the join work at all. If the harness ever wrote a transcript under a name other than its session id, `list` rows would carry a re-feedable-looking id that addresses a different session.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "82-87",
"snippet": " // Prefer the filename-derived id; cross-check with the data id (§2.4 spirit).\n let session_id = if session_id.is_empty() {\n data_session_id.unwrap_or_default()\n } else {\n session_id\n };"
},
{
"path": "src/live/registry.rs",
"lines": "57-59",
"snippet": " if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) {\n continue;\n }"
}
],
"instrument": "Python over every `~/.claude/projects/*/*.jsonl`: collect the set of top-level `sessionId` string values per file, compare it against the basename minus `.jsonl`, and report the files whose basename is absent from that set plus the files carrying more than one distinct value. Counting rule: one observation per file that carries at least one `sessionId` (a file with none is not counted either way). Measured 2026-09-02: 66 files observed, 0 mismatches, 0 files with two distinct values.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 - <<'EOF' (whole-corpus, one pass)\nimport os,re,json,glob\nroot=os.path.expanduser(\"~/.claude/projects\")\npat=re.compile(rb'\"sessionId\"\\s*:\\s*\"([^\"]*)\"')\nfor f in sorted(glob.glob(os.path.join(root,\"*\",\"*.jsonl\"))):\n base=os.path.basename(f)[:-6]; vals=set()\n for raw in open(f,\"rb\"):\n c=pat.findall(raw)\n if not c: continue\n s={x.decode(\"utf-8\",\"replace\") for x in c}\n if s=={base}: vals.add(base); continue\n try: o=json.loads(raw)\n except Exception: continue\n if isinstance(o,dict) and isinstance(o.get(\"sessionId\"),str): vals.add(o[\"sessionId\"])\n # tally: base not in vals -> mismatch; len(vals)>1 -> multi\nEOF\n; plus the registry join (python over ~/.claude/sessions/*.json: glob ~/.claude/projects/*/<row sessionId>.jsonl) ; plus a SCOPED subagent counter-check over the single project dir holding the most subagent transcripts ; plus `sed -n '82,87p' src/session/summarize.rs` and `rg -n 'sessionId' -B4 -A4 src/live/registry.rs`",
"observed": "files_scanned 66; lines_scanned 803946; files_with_sessionId 66; files_without_sessionId 0; basename_absent_from_set 0; files_with_multiple_distinct 0. Registry join: registry_rows_with_sessionId 7; rows_whose_sessionId_names_an_existing_top_level_transcript 7; rows_with_no_matching_transcript_filename 0; a registry row's sessionId is 36 chars and the row key set is [bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version]. Scoped subagent counter-check (one project dir, 5497 subagent transcripts present, first 400 checked): sessionId_equals_own_basename 0; sessionId_differs_from_basename 400; no_sessionId 0. Both code sites present verbatim at the claimed lines.",
"rule": "One observation per TOP-LEVEL transcript file matching ~/.claude/projects/<ENCODED>/<name>.jsonl (subagent transcripts under <uuid>/subagents/ are excluded by the glob depth). Per file, collect the set of TOP-LEVEL sessionId string values: a byte regex over each line is the fast path, and any line whose regex hits are not all equal to the basename is re-parsed with json.loads so that only the top-level key can enter the set (a sessionId quoted inside record content never counts). A file whose set is empty is counted in neither column. mismatch = basename absent from the set; multi = set size > 1. Registry rule: one observation per ~/.claude/sessions/*.json carrying a string sessionId; joined = at least one ~/.claude/projects/*/<sessionId>.jsonl exists.",
"note": "Reproduced exactly at CC 2.1.258: 66 files, 66 carrying a top-level sessionId, 0 basename mismatches, 0 files with two distinct values. The registry half of the 'depends' was instrumented too and the join is total on this machine (7 of 7 rows name an existing transcript filename), so filename-derived id == registry join key in practice, not just in principle. The claim's restriction to TOP-LEVEL transcripts is load-bearing and was measured: in a scoped check of one project dir, 400 of 400 subagent transcripts carry a sessionId that differs from their own basename (they carry the owning session's uuid), which is exactly why csift derives a subagent row's id from the path rather than from the record. Both code snippets are verbatim at the claimed paths and lines: src/session/summarize.rs:82-87 and src/live/registry.rs:57-59."
}
]
},
{
"id": "PATH-013",
"area": "path-encoding",
"behavior": "The transcript writer and the project-directory encoder handle astral-plane characters by OPPOSITE conventions: the writer emits them verbatim as 4-byte UTF-8 and never as an escaped surrogate pair (over the whole live corpus - 66 top-level transcripts, 803,946 lines - 8,633 lines, 1.074%, carry a 4-byte UTF-8 lead byte in 0xF0-0xF4, and 0 lines carry a real `\\udXXX` surrogate escape; exactly 1 line matches a naive surrogate regex, but its backslash run is EVEN, so those six characters are literal text quoted inside a tool result rather than an escape the writer produced), while the cwd encoder counts the same character as TWO UTF-16 code units and emits TWO dashes into the directory name.",
"depends": "csift must not carry either rule across: the verbatim-UTF-8 transcript convention is what keeps a non-ASCII (emoji, CJK) needle prefilter-eligible against raw line bytes - the needle-safety predicate escapes only `\"`, C0 controls and DEL - while the same emoji sitting in a cwd must be re-encoded unit-wise by `path::encode_cwd` to two dashes or the project directory is not found. Applying the encoder's UTF-16 view to search needles, or the writer's codepoint view to the encoder, breaks one surface silently in each direction.",
"code": [
{
"path": "src/search/matcher.rs",
"lines": "538-540",
"snippet": "pub(crate) fn json_escapes_in_string(c: char) -> bool {\n c == '\"' || (c as u32) < 0x20 || c == '\\u{7f}'\n}"
},
{
"path": "src/path/home.rs",
"lines": "15-27",
"snippet": "pub fn encode_cwd(cwd: &Path) -> String {\n use unicode_normalization::UnicodeNormalization;\n let s = cwd.to_string_lossy();\n let s: String = s.nfc().collect();\n let mut out = String::with_capacity(s.len());\n for unit in s.encode_utf16() {\n match u8::try_from(unit) {\n Ok(b) if b.is_ascii_alphanumeric() => out.push(char::from(b)),\n _ => out.push('-'),\n }\n }\n out\n}"
}
],
"instrument": "Writer side: python over every `~/.claude/projects/*/*.jsonl` - count a line if any of its raw bytes is in 0xF0..=0xF4, and separately if a match of `\\\\u[dD][89abAB][0-9a-fA-F]{2}` is preceded by an ODD run of consecutive backslash bytes (the parity guard is required: without it the corpus returns 1 false positive, a doubled backslash in quoted content). Counting rule: one observation per whole line, not prefix-bounded. Whole-corpus rather than a random sample, so a stranger reruns the identical number. Measured 2026-09-02 at CC 2.1.258: 803,946 lines, 8,633 four-byte-lead lines, 0 real surrogate escapes. Encoder side: `strings -n 6 <cc-binary> | rg -o 'function k\\(e\\)\\{return e\\.replace\\(/\\[\\^a-zA-Z0-9\\]/g,\"-\"\\)\\}.{0,300}'` recovers the sanitizer and its >200-cap wrapper, the `\"projects\"` path builder shows that wrapper is what names the project directory, a search for a unicode-flag variant (`/gu`, `/ug`, `/gv`) returns zero matches, and evaluating the extracted regex in a JS engine yields two dashes for one astral character against one dash for one BMP character. The csift unit test `encode_matches_cc_exactly_on_non_ascii_and_windows_paths` (src/path/tests/encode.rs) then pins the same law crate-side, but it is a regression guard on csift, not an instrument on Claude Code.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Writer side: python3 over every ~/.claude/projects/*/*.jsonl - per line, count a hit if any raw byte is in 0xF0..=0xF4, and separately run re.compile(rb'\\\\u[dD][89abAB][0-9a-fA-F]{2}') and then walk backwards from the match start counting the consecutive 0x5C bytes, classifying the match as a REAL JSON escape only when that run length is ODD. Encoder side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var v=m\\(\"claude-cli\"\\),C=200;.{0,340}' ; strings -n 6 ... | rg -o 'function k\\(e\\)\\{return e\\.replace\\(/\\[\\^a-zA-Z0-9\\]/g,\"-\"\\)\\}.{0,300}' ; strings -n 6 ... | rg -o '.{0,160}\"projects\".{0,160}' | rg -i 'join|normali|slice' ; strings -n 6 ... | rg -o '\\[\\^a-zA-Z0-9\\]/(gu|ug|gv)' ; then node -e on the extracted regex: (t)=>t.replace(/[^a-zA-Z0-9]/g,\"-\"). Code sites: sed -n '535,541p' src/search/matcher.rs ; sed -n '15,27p' src/path/home.rs ; rg -n 'encode_matches_cc_exactly_on_non_ascii_and_windows_paths' -A 14 src/path/tests/encode.rs",
"observed": "Writer side, whole corpus: files 66; lines_total 803946; lines_with_4byte_utf8_lead_F0_F4 8633 (1.074%); files_with_astral 44; lines_matching_naive_surrogate_regex 1; lines_with_REAL_surrogate_escape_odd_backslash_run 0. The single naive hit has consecutive_backslashes_before_u == 2 (even), the line parses as JSON, and the decoded record contains no astral character at all - the six characters are literal text inside a quoted tool result, not an escape the writer emitted. Encoder side, extracted verbatim from the binary: `var v=m(\"claude-cli\"),C=200;function S(t){let e=t.replace(/[^a-zA-Z0-9]/g,\"-\");if(e.length<=C)return e;return`${e.slice(0,C)}-${Math.abs(zq(t)).toString(36)}`}` and `function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")}function KA(e){let n=k(e);if(n.length<=IL)return n;return`${n.slice(0,IL)}-${be(e)}`}` and the projects-path builder `defaultPath(){let e=HU(),n=to(e,\"projects\"),r=mn(),d=this.canonicalWcRootForProject(r)??Jr(r)??r,f=e===Se()?Em(d):KA(d);return(to(n,f,cue)+Bl).normalize(\"NFC\")}` with `function Em(e){return Apr()??KA(e)}`. A search for a unicode-flag variant `[^a-zA-Z0-9]/gu`, `/ug` or `/gv` returned ZERO matches anywhere in the binary. node evaluating that exact extracted regex: astral_alone_dashes=2, bmp_cjk_char_dashes=1 (one astral char -> two dashes, one BMP CJK char -> one dash).",
"rule": "Writer side: one observation per whole line (not prefix-bounded) over all 66 top-level transcripts. A line counts as astral-bearing if any of its raw bytes lies in 0xF0..=0xF4 (the 4-byte UTF-8 lead range). A line counts as carrying a REAL surrogate escape only if a match of \\u[dD][89abAB][0-9a-fA-F]{2} is preceded by an ODD number of consecutive backslash bytes; an even run means the backslash is itself escaped, so the sequence is literal content text rather than an escape. Encoder side: the counting rule is the presence or absence of a matched literal in the stripped binary - a matched string is stable evidence, and the zero-match search for a unicode-flag variant is the negative control; the behavioural half is deterministic evaluation of the extracted regex, where dash count for a single character discriminates the UTF-16 code-unit view (2 for astral) from the codepoint view (1).",
"note": "Both halves of the claim survive, but the wording and the instrument needed correction on three points. (1) The writer-side surrogate count is 0 only under a backslash-parity guard: the naive regex the claim states returns exactly 1 hit over the whole corpus, and that hit is a doubled backslash - literal `\\u`-shaped text inside a quoted tool result, with no astral character in the decoded record. The claim's 'and 0 lines carry a surrogate escape' is true of the mechanism but false of the stated rule as written. (2) The claim's numbers came from a random 14-file, 43,547-line sample (286 hits, 0.66%), which no stranger can rerun to the same number; the whole-corpus census is deterministic and gives 8,633 of 803,946 lines, 1.074%, across 44 of 66 files. (3) The claim's encoder-side instrument was a csift unit test only - csift's own code, which by itself cannot decide how Claude Code behaves. Replaced with a binary extraction: the sanitizer `function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")}` and its cap wrapper `KA` are in the 2.1.258 binary, the projects-path builder resolves to `KA` on both branches (`Em` falls through to `KA`), the regex carries NO unicode flag (a search for /gu, /ug and /gv variants matched nothing in the whole binary), and evaluating that exact regex gives two dashes for one astral character. All three code snippets are verbatim at the claimed paths and lines: src/search/matcher.rs:537-539, src/path/home.rs:15-27, and the named unit test at src/path/tests/encode.rs:91-104 asserting `-tmp-x--y` for an astral char. Two incidental observations, neither a defect: the live corpus offers no discriminating power for the astral rule, because 0 of the 12 project directories whose records carry a cwd have a non-ASCII cwd, so the UTF-16 and codepoint encodings agree on all 12 - the binary and the regex evaluation are the only instruments that can decide it here; and the unit test's comment cites the 2.1.228 minified symbol names, whereas 2.1.258 names the same functions `k`, `KA` and `Em` - the asserted behavior is unchanged."
}
]
},
{
"id": "PLAN-001",
"area": "plan",
"behavior": "Entering Plan Mode makes Claude Code write a type:\"attachment\" record whose attachment payload is {\"type\":\"plan_mode\",\"reminderType\":\"full\"|\"sparse\",\"isSubAgent\":<bool>,\"planFilePath\":\"<absolute path>\",\"planExists\":<bool>} plus optional customInstructions / workshopDocPath / workshopOfferDocPath / workshopActiveDocPath. The record also carries the session's top-level slug, and planFilePath is always \"<slug>.md\". This attachment is the AUTHORITATIVE binding and takes precedence, but it is not the ONLY binding: when no plan_mode attachment exists Claude Code falls back to the first-valid-slug law (PLAN-006).",
"depends": "The byte prefilter token plan_mode also admits the sibling attachment types plan_mode_exit (23 records) and plan_mode_reentry (2), which likewise carry planFilePath; src/plan.rs then drops them with an exact att.get(\"type\")==Some(\"plan_mode\") check, so the binding stays correct. In this corpus no transcript carries an exit/reentry attachment without a plan_mode one (0 of 18), so the extra candidates never change an answer.",
"code": [
{
"path": "src/plan.rs",
"lines": "11-13",
"snippet": "//! {\"type\":\"attachment\",\"attachment\":{\"type\":\"plan_mode\",\n//! \"planFilePath\":\"/Users/…/.claude/plans/nested-prancing-popcorn.md\",\n//! \"isSubAgent\":false,\"planExists\":false}, …}"
},
{
"path": "src/plan.rs",
"lines": "110-118",
"snippet": " let Some(att) = rec.attachment_value() else {\n continue;\n };\n if att.get(\"type\").and_then(serde_json::Value::as_str) != Some(\"plan_mode\") {\n continue;\n }\n let Some(plan_file) = att.get(\"planFilePath\").and_then(serde_json::Value::as_str) else {\n continue;\n };"
},
{
"path": "src/plan.rs",
"lines": "82-90",
"snippet": "/// Tight byte prefilter for the plan-resolution pre-pass: `plan_mode` is a rare token, so\n/// a giant transcript parses only its handful of attachment lines (the scan still splits\n/// newlines over the whole file, but `serde_json` runs on almost nothing).\nfn line_is_plan_candidate(line: &[u8]) -> bool {\n // Built ONCE (per-line hot path - the stateless form rebuilt its searcher every call).\n static PLAN_MODE: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"plan_mode\"));\n PLAN_MODE.find(line).is_some()\n}"
}
],
"instrument": "`csift search '' <project dir> --count-by attachment | rg plan_mode` counts the binding records (counting rule: one per attachment record; measured 21 corpus-wide), then `csift plan @<id> --format json | jq '{plan_file, line, binding_source, slug, plan_exists}'` and `csift show @<id> --line <line> --raw | jq .attachment` must show that same payload.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "SPEC.md section 6.7.1; AGENTS.md section 5; src/plan.rs module doc; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' --count-by attachment (run from ~, whole corpus); then, over every plan_mode payload, rg -N --no-filename -o '\\{\"type\":\"plan_mode\"[^}]*\\}' -g '*.jsonl' . | jq -rc 'keys|join(\",\")' | sort | uniq -c; then per record: jq -r '(.slug) as $s | (.attachment.planFilePath|split(\"/\")|last|sub(\"\\\\.md$\";\"\")) as $b | if $b==$s then \"basename==slug\" else \"MISMATCH\" end'; and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{130}plan_mode.{130}'",
"observed": "Census: plan_mode=21, plan_mode_exit=23, plan_mode_reentry=2, plan_file_reference=151 attachment records. All 21 plan_mode payloads share one key set: isSubAgent,planExists,planFilePath,reminderType,type. 21/21 carry a top-level slug; 21/21 basename(planFilePath)==\"<slug>.md\". Binary emits: C.push({type:\"plan_mode\",reminderType:U,isSubAgent:!!r.agentId,planFilePath:f,planExists:_!==null,customInstructions:r.options.planModeInstructions,workshopDocPath:v,...W&&{workshopOfferDocPath:voe()},...z&&{workshopActiveDocPath:voe()}}). Counter-check: the naive byte pattern '\"type\":\"plan_mode\"' matches 67 records (3.2x over-count) because csift's own docs quote the shape and dev sessions read them back through tool results.",
"rule": "One row per attachment record, keyed on the PARSED attachment.type field (not raw bytes); key sets compared as the sorted comma-joined key list of the attachment payload object.",
"note": "Code sites verified verbatim: src/plan.rs 11-13, 107-115, 79-87 all match the current file exactly. The load-bearing half of the claim - that a path-shaped guess would mis-attribute edits to another session's plan - is untouched."
}
]
},
{
"id": "PLAN-002",
"area": "plan",
"behavior": "Within one transcript every plan_mode attachment carries the SAME planFilePath (18/18 transcripts, distinct-path cardinality 1). planExists is the field observed to flip false->true once the plan file is first written - observed in 2 of the 3 transcripts that carry more than one plan_mode record. reminderType is a second per-attachment field that is computed fresh on every attachment, so it is not guaranteed constant either; it was \"full\" on all 21 records here because no transcript in this corpus carries enough plan_mode attachments to reach the sparse branch.",
"depends": "csift takes the LATEST `plan_mode` attachment in file order as the current binding, so a session that entered Plan Mode repeatedly still reports one stable plan path with a current existence flag.",
"code": [
{
"path": "src/plan.rs",
"lines": "22-24",
"snippet": "//! Within one transcript every `plan_mode` attachment carries the same `planFilePath`\n//! (only `planExists` flips `false→true` once the plan is first written); we take the\n//! LATEST occurrence as the current binding."
},
{
"path": "src/plan.rs",
"lines": "107-109",
"snippet": " // File order == line order; overwriting keeps the LATEST plan_mode binding.\n let mut latest: Option<PlanRef> = None;\n for (line_no, rec) in &records {"
}
],
"instrument": "`jq -r 'select(.attachment.type==\"plan_mode\") | [.attachment.planFilePath, (.attachment.planExists|tostring)] | @tsv' <transcript> | sort | uniq -c` - expect one path carrying both flag values. Counting rule: one row per plan_mode attachment record.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "AGENTS.md section 5; src/plan.rs module doc; SPEC.md section 6.7.1; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "For each transcript matching '\"type\":\"plan_mode\"': rg -N --no-filename -o '\\{\"type\":\"plan_mode\"[^}]*\\}' \"$f\" | jq -r .planFilePath | sort -u | wc -l (distinct paths), and the same piped through jq -r '.planExists|tostring' | sort -u and jq -r .reminderType | sort -u",
"observed": "18 transcripts carry plan_mode attachments, 21 records total (15 transcripts x1 record, 3 transcripts x2 records). distinct planFilePath per transcript = 1 in 18/18. planExists value sets: 15 transcripts {false}, 2 transcripts {false,true} (the flip), 1 transcript {false} across both of its records. reminderType = {full} in 18/18.",
"rule": "One row per transcript carrying at least one plan_mode attachment; a field is called stable when its sort -u cardinality within that transcript is 1.",
"note": "The binary computes reminderType as U=(F!==null&&F.workshopDocPath!==v||I%X8n.FULL_REMINDER_EVERY_N_ATTACHMENTS===1)?\"full\":\"sparse\", where I counts plan_mode attachments since the last plan_mode_exit - so \"only planExists flips\" is a corpus observation, not a law. csift reads neither field, so the binding is unaffected. Code sites src/plan.rs 22-24 and 104-106 verified verbatim."
}
]
},
{
"id": "PLAN-003",
"area": "plan",
"behavior": "Claude Code writes Plan-Mode plans flat under ~/.claude/plans as <slug>.md, and a subagent's plan takes a -agent-<agentId> suffix (<slug>-agent-<agentId>.md). The name is three words ONLY when the slug was minted without a seed; with a seed it is <slugified-seed>-<adjective>-<noun>, so 4 of the 21 observed plan paths are not three-word. The suffix is the full agentId, which is a+16 hex for a plain subagent but embeds a dashed name for a teammate - not a bare hex in general. The name is not derivable from the session id. The plans directory is flat for Claude Code's own writes but is not exclusively Claude-Code-owned: 26 of 67 entries here are operator-created, including 2 subdirectories.",
"depends": "Unchanged and reinforced: csift must never derive a plan path from a session id or a directory listing, because the directory also holds operator files and nested directories that no session binds.",
"code": [
{
"path": "src/plan.rs",
"lines": "5-8",
"snippet": "//! Claude Code stores Plan-Mode plans flat under `~/.claude/plans/` with a random\n//! three-word name (`nested-prancing-popcorn.md`); a subagent's plan gets an\n//! `-agent-<hex>` suffix. The name is NOT derivable from the session id - it is bound\n//! to the session by a record the transcript writes on entering Plan Mode:"
}
],
"instrument": "`ls ~/.claude/plans | sed 's/\\.md$//' | awk -F- '{print NF}' | sort | uniq -c` - 3 fields for a session plan, more for an `-agent-<hex>` one. Counting rule: one row per .md file.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "AGENTS.md section 5; src/plan.rs module doc; SPEC.md section 6.7.1; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "ls -A ~/.claude/plans | rg -c '^[a-z]+-[a-z]+-[a-z]+\\.md$' and rg -c '^[a-z]+-[a-z]+-[a-z]+-agent-[0-9a-f]+\\.md$' and the inverse listing; find ~/.claude/plans -mindepth 2 -name '*.md' | wc -l; dirname/word-field census over all 21 planFilePath values; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{200}function n_\\(.{300}'",
"observed": "~/.claude/plans holds 67 top-level entries: 35 match <word>-<word>-<word>.md, 6 match <word>-<word>-<word>-agent-<id>.md, and 26 match neither (operator-parked .md backups, .js files, a .json, and 2 subdirectories holding 43 further .md files). All 21 planFilePath values sit directly in ~/.claude/plans - no subdirectory - so Claude Code's own writes are flat. Word-field counts of the 21 basenames: 17 have 3 fields, 2 have 4, 1 has 6, 1 has 10. Binary: function n_(n){let e=Q(),i=UN(e);if(Lf().markPlanPathServed(e),!n)return d(Ea(),`${i}.md`);return d(Ea(),`${i}-agent-${n}.md`)} - the name is <slug>.md, and the suffix is the agentId verbatim. Observed agentId shapes on disk: a+16hex (17 chars) and a<dashed-name>-<16hex> (31 and 42 chars seen); all 6 suffixed plan files use the 17-char form.",
"rule": "One row per entry in ~/.claude/plans for the name census; one row per plan_mode attachment record for the directory and word-field census; field count = awk -F- '{print NF}' on the basename with .md stripped.",
"note": "Code site src/plan.rs 5-8 verified verbatim. None of the 18 plan_mode-bearing transcripts is a subagent transcript (0 under a subagents/ path), so the -agent- suffix is evidenced by the binary and the 6 on-disk files rather than by any attachment record in this corpus."
}
]
},
{
"id": "PLAN-004",
"area": "plan",
"behavior": "The three-word plan name is drawn from a CSPRNG: randomBytes(4).readUInt32BE(0) % len indexes an adjective list, a gerund list and a mixed noun/surname list, with no session id anywhere in the draw. That pure three-word draw is the NO-SEED path. When getPlanSlug is called with a seed, the name is instead <slugified-seed>-<adjective>-<noun> (the gerund list unused), where the seed is slugified prompt text - so on that path the name DOES encode the opening words of the session, and 3 of 24 bound slugs here took it. The name is still never derivable from the session id.",
"depends": "csift never derives or guesses a plan file name; the binding record is the only source, and a derivation from the session id would name a wrong file on every session.",
"code": [
{
"path": "src/plan.rs",
"lines": "63",
"snippet": "/// The session's plan slug (the harness derives the plan file name from it)."
},
{
"path": "src/plan.rs",
"lines": "5-8",
"snippet": "//! Claude Code stores Plan-Mode plans flat under `~/.claude/plans/` with a random\n//! three-word name (`nested-prancing-popcorn.md`); a subagent's plan gets an\n//! `-agent-<hex>` suffix. The name is NOT derivable from the session id - it is bound\n//! to the session by a record the transcript writes on entering Plan Mode:"
}
],
"instrument": "`strings` the installed Claude Code 2.1.258 bundle and grep for the word lists (e.g. `prancing`, `foraging`): the namer draws with randomBytes, not a seeded PRNG, and takes no session-id argument. Counting rule: presence of the three word arrays and absence of any session-id argument in the namer.",
"located": {
"claude_code": "2.1.227",
"csift": "0.6.0",
"source": "SPEC.md section 6.7.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'randomBytes as .{0,3}[,}].{0,120}' and rg -o '.{200}readUInt32BE\\(0\\)%.{60}' and rg -o '.{0,900}\"babbage\".{0,900}' / '.{0,700}\"floofy\".{0,700}' / '.{150}\"foraging\".{150}' and rg -o '.{0,120}getPlanSlug\\(.{0,600}'",
"observed": "Verbatim: 'randomBytes as o}from\"crypto\";var t=[\"abundant\",\"ancient\",\"bright\",\"calm\",...' then 'function g(e){return o(4).readUInt32BE(0)%e}function a(e){return e[g(e.length)]}function xVt(){let e=a(t),n=a(s),i=a(l);return `${e}-${n}-${i}`}'. Three arrays: t = adjectives (jolly, bubbly, elegant, distributed, ... plus some participles like frolicking, humming), s = gerunds (foraging, prancing, snacking, dancing, wishing, ...), l = concrete nouns then computer-scientist surnames (popcorn, kettle, marshmallow, whistle, ... babbage, backus, pike, shamir, stallman, ...). xVt() takes no argument. But its caller does: getPlanSlug(n,e){...let s=e?O$e(e):\"\";...r=s?`${s}-${PU()}`:xVt()...} with function PU(){let e=a(t),n=a(l);return `${e}-${n}`} and O$e slugifying the first 4 words of the seed, lowercased, non-alnum collapsed to dashes, capped at 40 chars. Corpus: of 24 distinct bound slugs, 21 are strict three-word and 3 are seeded.",
"rule": "Counting rule as written in the claim - presence of the three word arrays and absence of any session-id argument in the namer - both pass. Slug shape counted as one row per distinct bound slug across the 26 top-level bindings, classified by the regex ^[a-z]+-[a-z]+-[a-z]+$.",
"note": "The claim's 'carries zero information about the session' and 'unknowable before Plan-Mode entry' are both too strong: the seeded path encodes prompt text, and PLAN-005 shows the slug exists before Plan-Mode entry. The mechanism itself - CSPRNG, the exact modulo formula, three lists, no session id - matched the binary exactly. csift's dependency (never derive or guess a name) is unaffected. Code sites src/plan.rs 61-66 and 5-8 verified verbatim."
}
]
},
{
"id": "PLAN-005",
"area": "plan",
"behavior": "slug is one stable value per session (cardinality 1 in every transcript checked), absent from most transcripts (38 of 64 top-level ones carry none) and from metadata-only records, and Claude Code derives the plan file NAME from it (21/21 plan_mode records have basename(planFilePath) == \"<slug>.md\", and the binary's plan-path getter is d(getPlansDirectory(), `${getPlanSlug(session)}.md`)). It is NOT minted at Plan-Mode entry: it is minted lazily on the first request for a plan slug and is already present on records written before the plan_mode attachment in 18 of 18 cases, sometimes by tens of thousands of lines. A compaction is one of the minters (5 of 26 first carriers are system/compact_boundary).",
"depends": "csift reads the slug off the binding record itself - the plan_mode attachment record always carries it - and its slug-only fallback walks the file from line 1 for the FIRST slug-carrying record, binding <plans-dir>/<slug>.md only when that record's slug passes the harness validity rule; a drift in when the slug is minted changes which record binds and therefore which plan file a session is reported bound to.",
"code": [
{
"path": "src/model/record.rs",
"lines": "45-50",
"snippet": " /// The session's plan slug (one stable value per session, minted BEFORE Plan Mode\n /// is entered - the first slug-carrying record precedes the plan_mode line in every\n /// measured session - and absent from every record before the mint and from\n /// metadata-only records). The harness derives the plan file name from it.\n #[serde(default)]\n pub slug: Option<String>,"
},
{
"path": "src/plan.rs",
"lines": "63",
"snippet": "/// The session's plan slug (the harness derives the plan file name from it)."
},
{
"path": "src/plan.rs",
"lines": "63-68",
"snippet": " /// The session's plan slug (the harness derives the plan file name from it).\n /// Read from the binding record itself: the `plan_mode` attachment record always\n /// carries it. The slug is minted BEFORE Plan-Mode entry (measured at CC 2.1.258:\n /// the first slug-carrying record precedes the `plan_mode` line in 18 of 18\n /// transcripts), so earlier records may carry it too - the binding record is\n /// simply the authoritative carrier. `None` when the record predates the field."
},
{
"path": "src/plan.rs",
"lines": "143-149",
"snippet": " if latest.is_none() {\n if let Some((line_no, rec)) = first_slug_record(bytes) {\n if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {\n // The project root the harness resolves plansDirectory against is the\n // session's ORIGINAL cwd - the transcript's first recorded cwd - not the\n // slug record's own cwd, which follows the tracked shell cwd and may\n // already sit in a subdirectory (section 3.11; v0.10.2)."
},
{
"path": "src/plan/slug.rs",
"lines": "6-11",
"snippet": "/// The FIRST record carrying a `slug` field (Claude Code's binding key), by a\n/// sequential early-exit walk - the fallback runs only when no `plan_mode` exists, and\n/// a slugged session's first carrier normally sits early after the mint point.\npub(crate) fn first_slug_record(bytes: &[u8]) -> Option<(usize, crate::model::Record)> {\n static SLUG: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"slug\\\"\"));"
}
],
"instrument": "`csift search '\"slug\"' @<session> --raw | head -1` gives the first slug-carrying line; compare its line number to the `line` reported by `csift plan @<session> --format json`. Counting rule: first slug-carrying line by file order.",
"located": {
"claude_code": null,
"csift": "0.9.4",
"source": "src/plan.rs and src/model/record.rs comments"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "Per plan_mode-bearing transcript: first top-level slug line via jq -rR 'input_line_number as $n | (fromjson? // empty) | select(has(\"slug\")) | $n' \"$f\" | head -1, compared with rg -n -m1 '\"type\":\"plan_mode\"' \"$f\" | cut -d: -f1; first-carrier type census over all 64 top-level transcripts; basename(planFilePath)==\"<slug>.md\" check over all 21 plan_mode records; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}getPlanSlug\\(.{0,600}'",
"observed": "In 18 of 18 plan_mode-bearing transcripts the first top-level slug record sits STRICTLY BEFORE the plan_mode line. Line pairs (first_slug -> first_plan_mode): 9->30, 10->14, 11->15, 39->49, 172->181, 179->186, 266->273, 268->274, 363->371 (x2), 717->718, 869->876 (x2), 1537->64713, 1549->11222, 1575->1584, 2042->2051, 2134->2141. First-carrier types among those 18: 11 assistant, 4 user, 2 system, 1 attachment - never the plan_mode record itself. Across all 64 top-level transcripts, 26 carry a top-level slug (first carriers: 12 assistant, 5 attachment, 5 system/compact_boundary, 4 user) and 38 carry none, while only 18 carry a plan_mode attachment - so at least 8 slug-bearing sessions never entered Plan Mode. Distinct slug values per transcript = 1 everywhere checked. Binary: getPlanSlug(n,e) mints on first request per session and caches it (i.set(n,r)); the plan-path getter n_() calls it unconditionally, so any code path that needs a plan path mints the slug.",
"rule": "One row per transcript. First slug carrier = the first line whose PARSED record has a top-level slug key (byte hits were confirmed top-level: 0 of 26 resolved to a nested occurrence). Strictly-before = first_slug_line < first_plan_mode_line.",
"note": "csift's CODE is unaffected: first_slug_record walks from line 1, so it finds the true first carrier regardless of where the mint happened, and the plan_mode branch reads the slug off the binding record. Only the doc comments misstate the mechanism - and the 'absent from the whole head window' clause is the actively misleading one, since a head-window reader could legitimately find the slug. code-site note: src/plan.rs 61-66 and src/model/record.rs 45-49 are verbatim-present but their prose is wrong. src/plan.rs 61-66 says 'the slug is minted at Plan-Mode entry, so it is absent from every earlier record (including the whole head window), but always present on the plan_mode attachment record'; only the last clause survives (21/21). src/model/record.rs 45-49 says 'minted when the session enters Plan Mode; ABSENT from every record before that entry'; both clauses are contradicted. Suggested replacement for both: 'one stable value per session, minted lazily the first time a plan slug is requested (Plan-Mode entry, a compaction, or any plan-path lookup) and cached for the session's life; absent from sessions that never needed one and from metadata-only records. Claude Code derives the plan file name from it.'"
}
]
},
{
"id": "PLAN-006",
"area": "plan",
"behavior": "When no `plan_mode` attachment exists anywhere in a transcript, Claude Code still binds a plan: its own rule takes the FIRST record in the log carrying a valid `slug` and binds `<plans-dir>/<slug>.md`, consulting no attachment.",
"depends": "csift adopted the same second law with disclosed provenance (`binding_source:\"slug-only\"`), so a session whose plan Claude Code will still inject is no longer answered with \"no plan\".",
"code": [
{
"path": "src/plan.rs",
"lines": "137-142",
"snippet": " // No `plan_mode` anywhere: fall back to Claude Code's ACTUAL binding law - the\n // FIRST record carrying a `slug` binds `<plans-dir>/<slug>.md` (the harness's\n // getSlugFromLog takes the first slug in the log, no attachment consulted). A\n // forked/background session reaches this state by construction (the clone strips\n // attachment history), and its slug is often minted by the first own compaction -\n // reporting \"no plan\" there is a WRONG answer: CC will inject/rebuild that file."
},
{
"path": "src/plan.rs",
"lines": "145",
"snippet": "if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {"
},
{
"path": "src/plan/slug.rs",
"lines": "6-13",
"snippet": "/// The FIRST record carrying a `slug` field (Claude Code's binding key), by a\n/// sequential early-exit walk - the fallback runs only when no `plan_mode` exists, and\n/// a slugged session's first carrier normally sits early after the mint point.\npub(crate) fn first_slug_record(bytes: &[u8]) -> Option<(usize, crate::model::Record)> {\n static SLUG: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"slug\\\"\"));\n let mut line_no = 0usize;\n for line in bytes.split(|&b| b == b'\\n') {"
}
],
"instrument": "`csift plan <project> --format json | jq 'select(.binding_source==\"slug-only\")'`, then confirm the named transcript carries zero plan_mode attachments (`csift search 'plan_mode' @<id> --attachments -c` = 0) and that its `plan_file` equals `<plans-dir>/<first valid slug>.md`. Counting rule: one row per resolved session.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger; CHANGELOG 0.9.4; src/plan.rs comment; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{900}getSlugFromLog: rejecting.{900}'; then, for each of the 64 top-level transcripts, csift plan \"$f\" --no-subagents --format json | jq -rc 'select(.kind==\"plan\")', cross-checked against rg -c '\"type\":\"plan_mode\"' \"$f\" and the first '\"slug\":\"' line number.",
"observed": "Binary, verbatim: var ue=/^[a-z0-9][a-z0-9-]{0,119}$/;function j(n){let e=n.messages.find((i)=>i.slug)?.slug;if(e===void 0)return;if(!ue.test(e)){t(`getSlugFromLog: rejecting malformed transcript slug (${e.length} chars)`);return}return e} and its caller: async function Z1e(n,e,i){let r=j(n);if(!r)return!1;let s=e??Q();if(yqt(s,r),i&&g())return pe(i,n,r).catch(...);let o=d(Ea(),`${r}.md`);...} where yqt is setPlanSlug and Ea is getPlansDirectory - the FIRST message carrying a slug, validated, then the plan file is <plans-dir>/<slug>.md, with no attachment consulted. On disk: 26 top-level bindings = 18 plan_mode + 8 slug-only (3 of the 8 minted_at_compaction). All 8 slug-only transcripts contain 0 plan_mode records; csift's reported line equals the file's first '\"slug\":\"' line in 8/8 (804, 1099, 1471, 1663, 2565, 2981, 3343, 4010); basename == \"<first slug>.md\" in 8/8, all under ~/.claude/plans. First carriers of those 8: 3 system/compact_boundary, 4 attachment, 1 assistant.",
"rule": "One row per top-level transcript resolved by csift plan; a binding counts as slug-only when binding_source==\"slug-only\", and is cross-checked by requiring rg -c '\"type\":\"plan_mode\"' == 0 on the same file.",
"note": "Both halves confirmed independently: Claude Code's own law from the binary (find-first + regex validation + <plans-dir>/<slug>.md) and csift's adoption of it on every slug-only session in the corpus. Code sites src/plan.rs 134-139, 140-155 and 161-168 verified verbatim."
}
]
},
{
"id": "PLAN-007",
"area": "plan",
"behavior": "A `slug` binds only if it passes Claude Code's validity rule: at most 120 characters, a lowercase-alphanumeric head, and a tail of lowercase alphanumerics or dashes.",
"depends": "csift's slug-only fallback applies the same predicate, so a tolerated `slug` value CC itself would reject never produces a bound plan path.",
"code": [
{
"path": "src/plan/slug.rs",
"lines": "27-37",
"snippet": "/// Claude Code's slug validity rule (lowercase alnum head, alnum/dash tail, <=120\n/// chars) - a stray tolerated `slug` value that CC itself would reject never binds.\npub(crate) fn slug_is_valid(s: &str) -> bool {\n let mut chars = s.chars();\n let Some(head) = chars.next() else {\n return false;\n };\n s.len() <= 120\n && (head.is_ascii_lowercase() || head.is_ascii_digit())\n && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')\n}"
}
],
"instrument": "Under `--claude-home <fixture>`, a transcript whose only slug fails the rule (uppercase head) must make `csift plan @<id>` report no binding, while the same fixture with a valid slug binds `<plans-dir>/<slug>.md`. Counting rule: one binding decision per transcript.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{900}getSlugFromLog: rejecting.{900}' for the regex; then five fixtures under csift --claude-home <fixture>/.claude plan @<id> --format json, each a two-line transcript whose only slug-carrying record has the slug under test and which carries no plan_mode attachment.",
"observed": "Claude Code's rule, verbatim: var ue=/^[a-z0-9][a-z0-9-]{0,119}$/ - a lowercase-alnum head plus at most 119 more characters from [a-z0-9-], i.e. 120 total. Fixture results: 'Jolly-prancing-popcorn' (uppercase head) -> no binding; '-jolly-prancing' (dash head) -> no binding; 'jolly_prancing' (underscore in tail) -> no binding; a 121-character all-'a' slug -> no binding; a 120-character all-'a' slug -> binds <claude-home>/plans/<120 a's>.md. csift's slug_is_valid (head is_ascii_lowercase||is_ascii_digit, tail all lowercase/digit/dash, s.len()<=120) accepts exactly the same language.",
"rule": "One binding decision per fixture transcript; 'no binding' means csift plan --format json emitted only the header and summary envelope lines and no kind==\"plan\" row.",
"note": "The byte-length vs code-unit question is moot: the accepted alphabet is ASCII-only, so csift's s.len()<=120 in bytes and the regex's {0,119} in UTF-16 code units agree on every string either one accepts. Code site src/plan.rs 182-192 verified verbatim."
}
]
},
{
"id": "PLAN-008",
"area": "plan",
"behavior": "Claude Code resolves `plansDirectory` from the merged settings scopes (user, then the project's `.claude/settings.json`, then `.claude/settings.local.json`, the more specific scope winning), joins a relative value to the process's current project cwd through the same getter that stamps every record's top-level `cwd` (the async-local cwd, else the project cwd, the original cwd only when that getter throws), refuses a result that escapes that cwd, and falls back to `<claude-home>/plans`. The resolved directory is memoized at its first access, so the binding follows the cwd as it stood early in the session, before any tracked-shell `cd` moved the per-record value.",
"depends": "csift's slug-only binding calls `plans_dir(root)` with `root` = the transcript's FIRST recorded `cwd` (`first_cwd`, the earliest instant csift can observe), reads the same three scopes in the same precedence from that root, joins and containment-checks against it, and falls back to `<claude-home>/plans`. Before v0.10.2 the root was the slug-carrying record's own `cwd`, which can already have drifted into a subdirectory: the plan file was then joined under the subdirectory and a project-scope `plansDirectory` was silently dropped. A session resumed from another directory re-memoizes in the harness, which the first-record instrument cannot see (documented limit).",
"code": [
{
"path": "src/plan/slug.rs",
"lines": "82-96",
"snippet": " let mut candidates: Vec<PathBuf> = vec![home.join(\"settings.json\")];\n candidates.push(root.join(\".claude\").join(\"settings.json\"));\n candidates.push(root.join(\".claude\").join(\"settings.local.json\"));\n let mut value: Option<String> = None;\n for p in candidates {\n let Ok(raw) = std::fs::read_to_string(&p) else {\n continue;\n };\n let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {\n continue;\n };\n if let Some(d) = v.get(\"plansDirectory\").and_then(serde_json::Value::as_str) {\n value = Some(d.to_string()); // later (more specific) scopes win\n }\n }"
},
{
"path": "src/plan/slug.rs",
"lines": "97-107",
"snippet": " let Some(d) = value else {\n return default;\n };\n let joined = crate::path::lexical_normalize(&root.join(d));\n let root_n = crate::path::lexical_normalize(root);\n if joined.starts_with(&root_n) {\n joined\n } else {\n default\n }\n}"
},
{
"path": "src/plan.rs",
"lines": "144-151",
"snippet": " if let Some((line_no, rec)) = first_slug_record(bytes) {\n if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {\n // The project root the harness resolves plansDirectory against is the\n // session's ORIGINAL cwd - the transcript's first recorded cwd - not the\n // slug record's own cwd, which follows the tracked shell cwd and may\n // already sit in a subdirectory (section 3.11; v0.10.2).\n let root_owned = first_cwd(bytes).or_else(|| rec.cwd.clone());\n let root = root_owned.as_deref().map(Path::new);"
},
{
"path": "src/plan/slug.rs",
"lines": "55-70",
"snippet": "pub(crate) fn first_cwd(bytes: &[u8]) -> Option<String> {\n let mut pos = 0usize;\n let mut seen = 0usize;\n while pos < bytes.len() && seen < 256 {\n let end = memchr::memchr(b'\\n', &bytes[pos..]).map_or(bytes.len(), |i| pos + i);\n let line = &bytes[pos..end];\n pos = end + 1;\n seen += 1;\n if let Ok(Some(rec)) = crate::parse::parse_line(line) {\n if let Some(c) = rec.cwd.as_deref().filter(|c| !c.is_empty()) {\n return Some(c.to_string());\n }\n }\n }\n None\n}"
}
],
"instrument": "Set `plansDirectory` in a fixture `settings.json` under `--claude-home`, then `csift plan @<id> --format json | jq -r .plan_file` names that directory; with the key unset the same session resolves under `<claude-home>/plans`. Counting rule: one directory lookup per invocation.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.4",
"source": "SPEC.md section 6 v0.9.4 ledger; src/plan.rs comment; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{600}plansDirectory must be within project root.{600}' and rg -o '.{300}plansDirectory.{0,200}' for the settings-schema description; then a csift fixture: csift --claude-home <fixture>/.claude plan @<id> --format json | jq -r .plan_file with plansDirectory unset, set to the relative value \"elsewhere\", and set to an absolute path.",
"observed": "Claude Code's settings schema, verbatim: plansDirectory:i().optional().describe(\"Custom directory for plan files, relative to project root. If not set, defaults to ~/.claude/plans/\"). Resolver, verbatim: #n(){let e=Je().plansDirectory;if(e){let i=ne(),r=O(i,e);if(le(r,i))return r;t(`plansDirectory must be within project root: ${e}`,{level:\"error\"})}return F()} with function F(){return d(Se(),\"plans\")} and the containment guard function le(n,e){if(n!==e&&!n.startsWith(e+B))return!1;...let i=NS(e);if(i===null)return!1;let r=n;for(;;){let s=NS(r);if(s!==null)return s===i||s.startsWith(i+B);let o=A(r);if(o===r)return!1;r=o}}. csift fixture: unset -> <claude-home>/plans/<slug>.md; \"elsewhere\" -> <claude-home>/elsewhere/<slug>.md; an absolute path -> that path verbatim.",
"rule": "One directory lookup per invocation; the fixture compares the basename-stripped directory of the reported plan_file across three settings.json states of one otherwise identical transcript.",
"note": "This machine has no plansDirectory set in ~/.claude/settings.json, so no transcript in the corpus exercises the setting and the divergence could not be observed on a Claude-Code-written record. It is established from two independent binary strings - the user-facing schema description and the resolver plus its error message - against a csift fixture. A live confirmation would need a session started in a project whose settings set plansDirectory, then comparing the plan_mode attachment's planFilePath against csift plan's answer."
},
{
"claude_code": "2.1.258",
"csift": "0.10.2",
"date": "2026-09-03",
"verdict": "drifted",
"instrument": "installed csift 0.10.1 against a synthetic --claude-home tree: a slug-only transcript whose record 1 carries cwd=<root> and record 2 (the slug carrier) cwd=<root>/sub; three runs of `csift plan @<id> --format json` with plansDirectory=myplans in <claude-home>/settings.json, then only in <root>/.claude/settings.json, then only in <root>/sub/.claude/settings.json; the harness mechanism read from the 2.1.258 binary's plans-directory resolver",
"observed": "run 1: plan_file <root>/sub/myplans/<slug>.md (joined under the drifted cwd); run 2: <claude-home>/plans/<slug>.md (the project-scope setting dropped); run 3: <root>/sub/subplans/<slug>.md (the settings anchor was the slug record's cwd); the binary resolves through the cwd getter that also stamps records and memoizes the directory on first access",
"rule": "one run = one plan call; a wrong binding = a plan_file not under <root>/myplans; the harness mechanism is read from the shipped binary, not run",
"note": "csift 0.10.2 binds against the transcript's first recorded cwd (plan/slug.rs first_cwd) and reads the settings scopes from that root; both wrong bindings are pinned by the e2e plan_slug_binding_resolves_plans_directory_against_the_first_recorded_cwd"
}
]
},
{
"id": "PLAN-009",
"area": "plan",
"behavior": "The plan approval carrier is a tool_result on a type:\"user\"/role:\"user\" record, with no is_error key and no typed user message. There are two variants: the normal one begins 'User has approved your plan.' and names where the plan was saved, and an empty-plan one reads 'User has approved exiting plan mode. You can now proceed.' and names no path. Both are structurally the harness greenlight, distinct from both rejection shapes.",
"depends": "csift never treats the approval as a user message or a turn boundary; counting `role:user` carriers instead would fabricate one human turn per approved plan.",
"code": [
{
"path": "src/model/tests/boundaries.rs",
"lines": "276-342",
"snippet": "#[test]\nfn plan_approval_is_not_a_boundary() {\n // The approval path is the harness greenlight (no typed message, no is_error) -\n // must NOT become a turn boundary.\n let r = parse(\n r#\"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"t\",\"content\":\"User has approved your plan. You can now start coding. Your plan has been saved to: /Users/testuser/.claude/plans/elegant-scribbling-dream.md\"}]}}\"#,\n );\n assert!(!r.is_plan_rejection_boundary());\n assert!(!r.is_auq_answer_boundary());\n assert!(!r.opens_turn());\n}"
}
],
"instrument": "rg -c 'User has approved your plan' over the corpus does NOT count the carriers - it over-counts 5.3x (64 byte hits vs 12 real carriers) because csift's own specs quote the string and dev sessions read those files back through tool results. Count carriers structurally: select tool_result blocks whose flattened content starts with the phrase.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 4.2.4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -c 'User has approved your plan' -g '*.jsonl' . (raw byte count) versus a structural jq census selecting tool_result blocks whose flattened content starts with 'User has approved'; then csift show \"$f\" --line \"$ln\" --format json | jq -rc '[(.labels|join(\"+\")),(.opens_turn|tostring)]' on every carrier; and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -n 'User has approved your plan' with the following strings-lines.",
"observed": "Binary, 2.1.258, two approval results, both {type:\"tool_result\",content:...,tool_use_id:v} with no is_error field: if(!n||n.trim()===\"\")return{type:\"tool_result\",content:\"User has approved exiting plan mode. You can now proceed.\",tool_use_id:v} and otherwise a template beginning 'User has approved your plan. You can now start coding. Start with updating your todo list if applicable' / 'Your plan has been saved to: ' / 'You can refer back to it if needed during implementation.'. On disk: the phrase occurs 64 times across 30 transcripts as raw bytes, but only 12 records carry it as an actual tool_result block's leading text - a 5.3x over-count. All 12 are type:\"user\", role:\"user\", have no is_error key, have string (not array) content, and do name the saved path. Zero records carry the phrase as a whole string message.content. csift labels all 12 agent.tool.result as a single label with opens_turn false.",
"rule": "One row per tool_result BLOCK whose content, flattened across the string and array forms, starts with 'User has approved' - not one row per raw byte occurrence. The label census is one row per such carrier.",
"note": "The 2.1.258 wording inserts 'Start with updating your todo list if applicable' after 'You can now start coding.'; the fixture in src/model/tests/boundaries.rs 323-333 uses the older wording. That is inert - csift matches nothing on this string, it simply asserts the record is not a boundary - but the fixture is now a historical rather than a current sample. The test itself is present verbatim at the claimed lines and still passes the behavior it pins: all 12 real carriers classify agent.tool.result and open no turn."
}
]
},
{
"id": "PLAN-010",
"area": "plan",
"behavior": "For an ExitPlanMode REJECTION the rejected `tool_use_id` is the id of the ExitPlanMode `tool_use`, whose `input.planFilePath` names the plan under discussion; an AskUserQuestion-clarify rejection carries no plan path.",
"depends": "csift builds a `PlanIndex` (`tool_use_id` to `planFilePath`) once per session and appends a `[plan: <path>]` pointer to the reconstructed rejection text, so a consuming LLM can go Read the rejected plan; with no index the rejection text is returned alone.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "225-235",
"snippet": " /// - an answered AskUserQuestion → the full Q+options+answer unit\n /// ([`Record::auq_exchange`]);\n /// - a tool-use rejection-with-message → the user's typed instruction, optionally\n /// suffixed with a `[plan: <path>]` pointer when `plan_index` resolves the rejected\n /// `tool_use_id` to an ExitPlanMode plan (§4.2.4). `plan_index` may be `None` (no\n /// plan resolution attempted), in which case the rejection text is returned alone.\n ///\n /// Returns `None` when this record does not open a turn. CODEPOINT-SAFE throughout\n /// (delegates to the codepoint-safe accessors).\n #[must_use]\n pub fn reconstructed_user_text(&self, plan_index: Option<&PlanIndex>) -> Option<String> {"
},
{
"path": "src/model/grouping.rs",
"lines": "5-9",
"snippet": "/// An index of ExitPlanMode `tool_use_id → planFilePath` built from a session's records\n/// (§4.2.4). A tool-use rejection-with-message ([`Record::plan_rejection_message`])\n/// resolves the rejected `tool_use_id` through this index to surface a `[plan: <path>]`\n/// pointer so a consuming LLM can go Read the plan. Built once per session via\n/// [`PlanIndex::from_records`]; cheap (one `BTreeMap` of the few ExitPlanMode calls)."
},
{
"path": "src/model/exchange.rs",
"lines": "281-286",
"snippet": " /// The ExitPlanMode tool_use blocks carried by this (assistant) record, as\n /// `(tool_use_id, plan_file_path)` pairs - the raw material a [`PlanIndex`] is built\n /// from. `plan_file_path` prefers `input.planFilePath`; a block with no path yields\n /// an empty string (still indexed so the id is known to be an ExitPlanMode). Empty\n /// for any record carrying no ExitPlanMode tool_use.\n #[must_use]"
}
],
"instrument": "`csift search 'To tell you how to proceed' @<id>` - an ExitPlanMode rejection renders the typed instruction with a trailing `[plan: <path>]`, an AUQ-clarify rejection renders none. Counting rule: one pointer per rejection whose rejected id is in that session's ExitPlanMode index.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 4.2.4; src/model/exchange.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Structural jq census over every '\"name\":\"ExitPlanMode\"' record for input.planFilePath / input.plan; then, for every tool_result carrying 'To tell you how to proceed' with is_error==true, a per-file join of its tool_use_id against that file's tool_use id->name map; then csift show \"$f\" --line \"$ln\" | rg -c '\\[plan: ' on each; and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}case kc:\\{let d=n_\\(r\\).{0,180}'",
"observed": "28 of 28 ExitPlanMode tool_use blocks on disk carry both input.planFilePath and input.plan. Binary, verbatim: case kc:{let d=n_(r);Lp(d);let f=BN(r);return H1(o),f!==null?{...n,plan:f,planFilePath:d}:n} - normalizeToolInput injects the plan path and content together, or neither. Genuine rejections (tool_result with is_error:true carrying the 'To tell you how to proceed, the user said:' tail): 10 total - 3 rejected an ExitPlanMode tool_use id, and all 3 of those tool_uses carry planFilePath; 7 rejected an AskUserQuestion tool_use id, and none of those carries a planFilePath. csift renders a trailing [plan: <path>] pointer on 3 of 3 ExitPlanMode rejections and 0 of 7 AskUserQuestion rejections.",
"rule": "One row per tool_result block containing the typed-tail phrase AND carrying is_error:true, joined by tool_use_id to the issuing tool_use's name within the same file; the pointer test is one row per such rejection.",
"note": "Counting-rule trap worth recording: without the is_error:true requirement the same phrase also matches 59 Bash and 80 Read tool_results, which are file contents echoed back rather than rejections - requiring is_error:true is what isolates the real ones. Edge case not observed here (0 of 28): the binary omits BOTH plan and planFilePath from the ExitPlanMode input when the plan content cannot be read, so a rejection of such a call would carry no pointer; csift still indexes the id (an empty path) and returns the rejection text alone, which is the correct degradation. Code sites src/model/exchange.rs 225-235 and 281-286 and src/model/grouping.rs 5-9 verified verbatim."
}
]
},
{
"id": "PLAN-011",
"area": "plan",
"behavior": "A session carries exactly one slug value for its whole life: across the corpus 26 top-level transcripts carry a `slug` and zero carry more than one distinct value (counting rule: distinct slug values per file).",
"depends": "csift does not model slug transitions, and its fallback stops at the FIRST slug-carrying record via an early-exit walk - sound only because the value never changes.",
"code": [
{
"path": "src/plan/slug.rs",
"lines": "6-13",
"snippet": "/// The FIRST record carrying a `slug` field (Claude Code's binding key), by a\n/// sequential early-exit walk - the fallback runs only when no `plan_mode` exists, and\n/// a slugged session's first carrier normally sits early after the mint point.\npub(crate) fn first_slug_record(bytes: &[u8]) -> Option<(usize, crate::model::Record)> {\n static SLUG: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"slug\\\"\"));\n let mut line_no = 0usize;\n for line in bytes.split(|&b| b == b'\\n') {"
}
],
"instrument": "`for f in ~/.claude/projects/*/*.jsonl; do jq -r 'select(.slug)|.slug' \"$f\" | sort -u | wc -l; done | sort | uniq -c` - only 0 and 1 appear. Counting rule: distinct slug values per transcript.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "CHANGELOG 0.9.4; SPEC.md section 6 v0.9.4 ledger; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "cd ~/.claude/projects && rg -l '\"slug\"' --glob '*.jsonl' --max-depth 2 . > slugfiles.txt; wc -l < slugfiles.txt; for f in $(cat slugfiles.txt); do rg --no-filename '\"slug\"' \"$f\" | jq -r 'select(.slug)|.slug' | sort -u | wc -l; done | sort | uniq -c",
"observed": "26 top-level transcripts contain the byte sequence \"slug\"; the per-file distinct-top-level-slug histogram is `26 1` - every one of the 26 reports exactly 1 distinct value, none reports 0 or >=2.",
"rule": "One row per top-level transcript (a `*.jsonl` at depth 2 under ~/.claude/projects, i.e. <encoded-project-dir>/<session>.jsonl; subagent transcripts sit at depth 4 and are excluded). Row value = number of distinct values of the TOP-LEVEL `.slug` field, counted by `jq 'select(.slug)|.slug' | sort -u | wc -l` over the `\"slug\"`-prefiltered lines.",
"note": "Numbers reproduce the claim exactly (26 transcripts, zero with more than one value). One trap worth recording for anyone rerunning this: the counting rule must be the top-level `.slug` field, not a byte scan. `rg -o '\"slug\"\\s*:\\s*\"[^\"]*\"'` returns TWO distinct values on one of the 26 transcripts - the session's real three-word slug (2624 occurrences) and that same slug prefixed with a progress counter, `483/483 <slug>` (21 occurrences). The second lives inside nested tool-call and attachment payloads, never in the top-level field; re-run through `jq 'select(.slug)'` and that file collapses to 1. The claim's own instrument already uses jq, so the behavior sentence stands as written. Code site src/plan.rs:161-168 confirmed verbatim (the `first_slug_record` early-exit walk over a `\"slug\"` memmem finder)."
}
]
},
{
"id": "PLAN-012",
"area": "plan",
"behavior": "csift's `minted_at_compaction` flag means exactly what its code says - the first slug-carrying record is itself a `system`/`compact_boundary` - and it is only ever set on the `slug-only` fallback path. Measured over the 26 slug-bearing top-level transcripts, the first slug carrier is one of FOUR record types: `assistant` 12, `attachment` 5, `system`/`compact_boundary` 5, `user` 4. Exactly 8 transcripts carry no `plan_mode` attachment at all and therefore bind `slug-only`; of those, 3 are boundary-first (`minted_at_compaction:true`), 1 is assistant-first and 4 are attachment-first (`minted_at_compaction:false`). Measured examples: a boundary-first slug-only transcript with 2083 slug-bearing records and one distinct value; an assistant-first transcript with 69933 slug-bearing records and one distinct value.",
"depends": "csift sets `minted_at_compaction` when the first slug carrier is a compact boundary. It is a positive signal that a slug arrived at a compaction rather than at Plan-Mode entry, but it is NOT a two-way discriminator for whether Plan Mode ran - `binding_source` is what carries that, and a `slug-only` binding with `minted_at_compaction:false` is the common case (5 of 8 measured).",
"code": [
{
"path": "src/plan.rs",
"lines": "76-80",
"snippet": " /// True when the first slug-carrying record is a `compact_boundary` itself: the\n /// slug was MINTED at that compaction (Plan Mode never ran; Claude Code will\n /// still inject or rebuild the file it names) - the forked-session signature.\n pub minted_at_compaction: bool,\n}"
},
{
"path": "src/plan.rs",
"lines": "145",
"snippet": "if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {"
}
],
"instrument": "For each slug-bearing transcript take the first line containing `\"slug\"` and print its `type`/`subtype`: `system/compact_boundary` marks the minted-at-compaction case (counting rule: one row per slug-bearing transcript, first carrier only). Then `csift plan @<id> --format json | jq '{slug, binding_source, minted_at_compaction}'`.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "CHANGELOG 0.9.4; SPEC.md section 6 v0.9.4 ledger; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && for f in $(cat slugfiles.txt); do first=$(rg --no-filename '\"slug\"' \"$f\" | jq -rc 'select(.slug)|[.type,(.subtype//\"-\")]|@tsv' | head -1); att=$(rg --no-filename 'plan_mode' \"$f\" | jq -r 'select(.attachment.type==\"plan_mode\")|\"x\"' | wc -l); row=$(csift plan \"$f\" --no-subagents --format json | jq -rc 'select(.kind==\"plan\")|[.binding_source,(.minted_at_compaction|tostring)]|@tsv' | head -1); printf '%s\\t%s\\t%s\\n' \"$first\" \"$att\" \"$row\"; done | sort | uniq -c",
"observed": "26 slug-bearing top-level transcripts. First-slug-carrier record type crossed with the csift binding: `assistant` 11 -> plan_mode/false and 1 -> slug-only/false; `attachment` 1 -> plan_mode/false and 4 -> slug-only/false; `system`+`compact_boundary` 2 -> plan_mode/false and 3 -> slug-only/TRUE; `user` 4 -> plan_mode/false. So first-carrier types are assistant 12, attachment 5, system/compact_boundary 5, user 4; `plan_mode` attachment records number 0 in exactly 8 transcripts, and those 8 are precisely the 8 that read `slug-only`. Slug-bearing record counts on the five boundary-first transcripts: 169, 2083, 2670, 31385, 94111. One assistant-first transcript carries 69933 slug-bearing records with one distinct value.",
"rule": "One row per slug-bearing top-level transcript (the same 26 as PLAN-011). First carrier = the `type`/`subtype` of the FIRST line whose top-level `.slug` is non-null, in file order. `plan_mode` attachment count = records with `attachment.type==\"plan_mode\"`. Binding = the `binding_source`/`minted_at_compaction` of the single `kind:\"plan\"` row csift emits for that file under `--no-subagents`.",
"note": "The mechanism csift implements is confirmed by instrument (3 transcripts read slug-only with minted_at_compaction:true, and all 3 are boundary-first with zero plan_mode attachments), and the claim's 69,933-record assistant-first transcript still exists with one distinct slug value. What is refuted is the claim's implied biconditional, in both directions: 2 of the 5 boundary-first transcripts DO carry `plan_mode` attachments, so they bind via `plan_mode` and never reach the slug fallback at all; and 5 of the 8 transcripts with no `plan_mode` attachment have a non-boundary first carrier. The `attachment`-first bucket has a clean explanation the instrument surfaced: on one transcript the `plan_mode` attachment record and the first slug carrier are the SAME record (identical timestamp, `type:\"attachment\"`), which is precisely what src/plan.rs:61-66 documents. The claim's 480-slug-record forked transcript is no longer identifiable; boundary-first transcripts now carry 169 / 2083 / 2670 / 31385 / 94111 slug-bearing records. Code sites src/plan.rs:73-77 and src/plan.rs:140-155 confirmed verbatim."
}
]
},
{
"id": "PLAN-013",
"area": "plan",
"behavior": "A transcript minted by copying another session at a compaction has NO `plan_mode` attachment of its own - the fork strips the attachment history - so Claude Code's binding for it falls to the first-valid-slug law. The fork strips the SLUG as well: the clone's head `compact_boundary` (its first timestamped record) carries `slug:null`, and its first slug-carrying record arrives 1443 lines and roughly 23 hours later at the clone's OWN next compaction, minting a slug DIFFERENT from the origin's. Claude Code does re-inject a plan into the clone in full, twice over: at the fork instant it re-injects the plan bound to the session it was forked FROM, and after each later compaction it re-injects the clone's own newly-minted plan.",
"depends": "csift's `slug-only` fallback plus `minted_at_compaction` exists for exactly this shape; binding on the attachment alone answered \"no plan\" for a session whose plan Claude Code re-injects in full.",
"code": [
{
"path": "src/plan.rs",
"lines": "70-75",
"snippet": " /// How the binding was established: `plan_mode` (the explicit attachment, path\n /// verbatim) or `slug-only` (no `plan_mode` anywhere; the FIRST slug-carrying\n /// record binds `<plans-dir>/<slug>.md` - Claude Code's own binding law, which a\n /// forked/background session reaches because the fork strips its history's\n /// attachments, and any compaction mints a slug even without Plan Mode).\n pub binding_source: &'static str,"
},
{
"path": "src/plan.rs",
"lines": "137-142",
"snippet": " // No `plan_mode` anywhere: fall back to Claude Code's ACTUAL binding law - the\n // FIRST record carrying a `slug` binds `<plans-dir>/<slug>.md` (the harness's\n // getSlugFromLog takes the first slug in the log, no attachment consulted). A\n // forked/background session reaches this state by construction (the clone strips\n // attachment history), and its slug is often minted by the first own compaction -\n // reporting \"no plan\" there is a WRONG answer: CC will inject/rebuild that file."
}
],
"instrument": "Pick a session `csift list --format json | jq -r 'select(.clone_of)'` reports as a clone, then `csift plan @<that id> --format json | jq -r '[.binding_source, (.minted_at_compaction|tostring)] | @tsv'` reads `slug-only`. Counting rule: one binding per transcript.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.4",
"source": "AGENTS.md section 3.5; CHANGELOG 0.9.4; csift GOLD plan v0.9.4 round; dev sessions 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift list --format json --no-subagents --max-count 0 | jq -rc 'select(.clone_of)|[.session_id,.clone_of]|@tsv' ; csift plan <clone>.jsonl --no-subagents --format json | jq -rc 'select(.kind==\"plan\")|{binding_source,minted_at_compaction,plan_exists,line}' ; rg --no-filename 'plan_mode' <clone>.jsonl | jq -r 'select(.attachment.type==\"plan_mode\")|\"x\"' | wc -l ; python3 walk over the clone printing, in file order, every `compact_boundary` (line, slug, trigger, ts) and every `attachment.type==\"plan_file_reference\"` (line, planFilePath basename, len(planContent), ts)",
"observed": "Exactly 1 clone in the corpus. Clone: 0 `plan_mode` attachment records; csift reports {\"binding_source\":\"slug-only\",\"minted_at_compaction\":true,\"plan_exists\":true,\"line\":1471}. Its origin reports {\"binding_source\":\"plan_mode\",\"minted_at_compaction\":false,\"plan_exists\":true,\"line\":1584}, and the two bind DIFFERENT plan files (different three-word slugs). Clone in file order: L28 BOUNDARY slug=null trigger=auto 2026-08-31T11:41:41Z (its first timestamped record); L43 plan_file_reference naming the ORIGIN's plan file, planContent 521705 chars, 2026-08-31T11:41:39Z; L1471 BOUNDARY slug=<the clone's own> trigger=auto 2026-09-01T10:17:05Z - the first slug-carrying record, 1443 lines and 22h35m after the head boundary; L2175 BOUNDARY manual then L2185 plan_file_reference naming the clone's own plan, 553003 chars; L3254 BOUNDARY manual then L3264 same, 487001 chars; L5251 BOUNDARY auto then L5258 same, 493085 chars. Clone total 5530 lines.",
"rule": "One binding per transcript (csift's single `kind:\"plan\"` row under `--no-subagents`). Clone set = transcripts for which `csift list --format json` emits a non-null `clone_of`. Re-injection events = records with `attachment.type==\"plan_file_reference\"`, one row each, compared by `planFilePath` against that transcript's own csift-resolved bound plan.",
"note": "Both halves that matter for csift are confirmed by instrument: the one clone in the corpus has zero `plan_mode` attachments, binds `slug-only` with `minted_at_compaction:true`, and the file its slug names exists on disk. The clause needing correction is 'keeping the slug-carrying records' - the clone kept none of the origin's. The 'will still inject or rebuild the file that slug names' half is now confirmed directly rather than inferred: the clone carries four `plan_file_reference` attachments, each a full `planContent` payload of roughly half a megabyte, three naming its own minted plan and landing 7-10 lines after a `compact_boundary`. The fourth, 15 lines after the head boundary, names the ORIGIN's plan - the one case in the whole corpus where a re-injection does not name the transcript's own csift-resolved binding, and it is exactly the fork instant, before the clone's own slug existed. Code sites src/plan.rs:67-72 and src/plan.rs:134-139 confirmed verbatim."
}
]
},
{
"id": "PLAN-014",
"area": "plan",
"behavior": "The plan name is minted and the `plan_mode` attachment written at Plan-Mode ENTRY, while the `.md` file itself lands on disk only when plan content is first written - an arbitrary gap in which a session is bound to a plan file that does not exist (the attachment's own `planExists` is false).",
"depends": "csift reports `plan_exists:false` (text `[missing]`) as the accurate state of that window rather than erroring, and `recover --file @plan` can still rebuild the content from the transcript alone.",
"code": [
{
"path": "src/plan.rs",
"lines": "58-60",
"snippet": " /// Whether that plan file currently exists on disk (a recover target need NOT exist -\n /// recovering a deleted plan from the transcript is the whole point).\n pub plan_exists: bool,"
},
{
"path": "src/plan.rs",
"lines": "63",
"snippet": "/// The session's plan slug (the harness derives the plan file name from it)."
}
],
"instrument": "Decidable from the corpus alone, no live session required: the `plan_mode` attachment carries its own `planExists` field, so `rg --no-filename 'plan_mode' <transcript> | jq -rc 'select(.attachment.type==\"plan_mode\")|(.attachment.planExists|tostring)'` reads the state of the window retroactively. The first `plan_mode` attachment of a transcript is `planExists:false` in 18 of 18 measured transcripts, and the flip to `true` (seen on 2 transcripts) always comes later in file order.",
"located": {
"claude_code": "2.1.227",
"csift": "0.8.1",
"source": "SPEC.md section 6.7.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && for f in $(find . -maxdepth 2 -name '*.jsonl'); do rg --no-filename 'plan_mode' \"$f\" | jq -rc 'select(.attachment.type==\"plan_mode\")|(.attachment.planExists|tostring)'; done | sort | uniq -c [and the same per-file preserving file order, plus `head -1` for the first attachment] ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'planExists|plan_mode|planFilePath|plansDirectory|getSlugFromLog' | sort | uniq -c",
"observed": "21 `plan_mode` attachment records across 18 top-level transcripts: 19 carry `planExists:false`, 2 carry `true`. Taking only the FIRST `plan_mode` attachment per transcript: 18 of 18 are `false`. Of the 3 transcripts with two or more such attachments, 2 show the ordered sequence `false>true` and 1 shows `false>false`; no transcript shows `true` before `false`. In one transcript the `plan_mode` attachment record and that transcript's first slug-carrying record are the SAME record (identical timestamp 2026-07-28T13:12:49.650Z, `type:\"attachment\"`), carrying `planFilePath`, `planExists:false` and the slug together. Binary 2.1.258 strings: `plan_mode` 77, `planFilePath` 30, `planExists` 9, `plansDirectory` 7, `getSlugFromLog` 2.",
"rule": "One row per `plan_mode` attachment record (`attachment.type==\"plan_mode\"`) in a top-level transcript, value = `attachment.planExists`; plus one row per transcript taking only the first such record in file order. Binary counts are occurrences of each literal in `strings -n 6` output.",
"note": "The behavior holds and is now measured rather than asserted. What was wrong is the claim's own instrument line, 'Only a live interactive session can show the window' - the attachment records the window's state itself. The gap is real and it is the default: every transcript that entered Plan Mode was bound to a not-yet-existing plan file at entry, and only 2 of 18 were later observed writing it while still emitting a further attachment. Binary 2.1.258 still ships every string this mechanism is named by, including `plansDirectory` (why csift refuses to guess the plans directory) and `getSlugFromLog` (the first-slug binding law the fallback mirrors). Code sites src/plan.rs:56-58 and src/plan.rs:61-66 confirmed verbatim; the same-record observation directly confirms the 61-66 doc comment's claim that the slug is 'always present on the `plan_mode` attachment record'."
}
]
},
{
"id": "PLAN-015",
"area": "plan",
"behavior": "A session may freely `Edit`/`Write` ANOTHER session's plan file; those land as ordinary structured tool calls on a path under the plans directory, indistinguishable by path from the session's own plan.",
"depends": "csift refuses path heuristics for the binding, and `plan --audit` identifies plan files by a JOIN against the corpus's `plan_mode` bindings (one `plan_mode`-prefiltered scan of every project) rather than a plans-directory guess, warning when the mutating session does not bind the file.",
"code": [
{
"path": "src/plan.rs",
"lines": "16-20",
"snippet": "//! This `plan_mode` attachment is the AUTHORITATIVE binding. Crucially it is the *only*\n//! reliable one: a session may freely `Edit`/`Write` OTHER sessions' plan files (they\n//! show up as ordinary tool calls on a `~/.claude/plans/…` path), so \"any plans/ path the\n//! session touched\" is NOT the session's own plan. The bound plan is the one named in the\n//! `plan_mode` attachment, full stop - no path heuristics."
},
{
"path": "src/plan/audit.rs",
"lines": "3-13",
"snippet": "//! Why this audit exists: a session may freely `Edit`/`Write` ANOTHER session's plan\n//! file (it is an ordinary tool call on a path), but after a compaction only the\n//! session's OWN bound plan is re-injected in full - content parked in an unbound plan\n//! file does not come back. The audit finds every structured mutation the target scope\n//! made to a file that SOME session binds as its plan, and warns when the mutating\n//! session does not bind that file itself.\n//!\n//! Identification is a JOIN against the corpus's `plan_mode` bindings (one scan of\n//! every project, `plan_mode`-prefiltered so it parses almost nothing), never a plans\n//! directory guess (`plansDirectory` is configurable). Bash-side edits are outside\n//! this audit: structured `Write`/`Edit`/`MultiEdit`/`NotebookEdit` only."
}
],
"instrument": "`csift plan --audit` with NO positional target resolves the CALLING session from the environment and audits only that scope - it is not a corpus audit, and it reports `warnings:0` even when cross-session plan edits exist elsewhere. To get the corpus answer, pass every project directory explicitly as an `@<encoded-dir>` positional: cd ~/.claude/projects && dirs=$(find . -maxdepth 1 -mindepth 1 -type d -exec basename {} \\; | sed 's/^/@/' | tr '\\n' ' '); csift plan --audit --format json ${=dirs} | jq 'select(.kind==\"plan-edit\" and .bound_by_owner==false)'. In zsh the `${=dirs}` word-split form is required; a bare `$dirs` is passed as one argument and the resolver rejects it.",
"located": {
"claude_code": null,
"csift": "0.8.1",
"source": "src/plan.rs module doc; src/plan/audit.rs module doc; CHANGELOG 0.8.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && dirs=$(find . -maxdepth 1 -mindepth 1 -type d -exec basename {} \\; | sed 's/^/@/' | tr '\\n' ' '); csift plan --audit --format json ${=dirs} | jq -rc 'select(.kind==\"plan-edit\")|[.mutations,(.bound_by_owner|tostring)]|@tsv' [contrast: csift plan --audit --format json with NO target] ; csift files --by file --regex '\\.claude/plans/' --format json | jq -rc 'select(.kind==\"file\")|[.path,.write,.edit,.bash,.total]|@tsv'",
"observed": "With no target: summary {\"bindings\":73,\"plan_files_touched\":1,\"warnings\":0} and a single plan-edit row with `bound_by_owner:true` - zero evidence either way. With all 14 project dirs passed as explicit `@<encoded-dir>` targets: summary {\"bindings\":6004,\"plan_files_touched\":18,\"warnings\":2}, 18 plan-edit rows, of which 2 carry `bound_by_owner:false` with 262 and 31 structured mutations respectively; each of those two plan files ALSO has a `bound_by_owner:true` row from its binder (11 and 380 mutations). Separately `csift files --by file --regex '\\.claude/plans/'` returns 69 rows, and several distinct plan files appear on two rows each with different `session_id` - the same plan file mutated from two different transcripts.",
"rule": "One plan-edit row per (owning transcript, plan file) pair with at least one structured `Write`/`Edit`/`MultiEdit`/`NotebookEdit` mutation to a file that some session binds via `plan_mode`; `bound_by_owner` is false when the mutating transcript does not itself bind that file. Bash-side mutations are outside the audit. The `csift files` rows are one per (transcript, absolute path) pair whose full path matches the regex.",
"note": "The behavior is confirmed by instrument once the scope is right: 2 of 18 (transcript, plan file) pairs in the corpus are a transcript making structured mutations to a plan file it does not bind, one of them 262 mutations deep. `csift files --by file --regex '\\.claude/plans/'` independently shows plan files are ordinary structured mutation targets - 69 (transcript, path) rows under the plans directory, several plan files carrying rows from two different transcripts. The refinement is entirely about the instrument: as written it audits the calling session only and returns an empty set, which would read as a refutation of the very claim it was meant to support. csift's design choice is separately supported at the binary level - `plansDirectory` appears 7 times in 2.1.258, so the plans directory genuinely is configurable and a path guess would be unsound. Code sites src/plan.rs:16-20 and src/plan/audit.rs:3-13 confirmed verbatim."
}
]
},
{
"id": "PLAN-016",
"area": "plan",
"behavior": "After a compaction Claude Code re-injects the session's BOUND plan file in full, as a `type:\"attachment\"` record whose `attachment.type` is `plan_file_reference`, carrying `planFilePath` plus the entire plan text in `planContent` (measured payloads 487k-553k characters). Measured over 151 such records in top-level transcripts, 150 name the transcript's own bound plan and 150 land 4-15 lines after a `compact_boundary`. The single exception is a forked transcript's first re-injection, which names the plan bound to the session it was forked from - so the rule is better stated as 'the plan bound to the conversation being continued', which for every non-forked transcript is its own. Content parked in a plan file the session does not bind is never re-injected.",
"depends": "`plan --audit` exists to surface that hazard: it warns for every structured mutation the target scope made to a plan file the mutating session does not bind, and names the session that does bind it.",
"code": [
{
"path": "src/plan/audit.rs",
"lines": "3-13",
"snippet": "//! Why this audit exists: a session may freely `Edit`/`Write` ANOTHER session's plan\n//! file (it is an ordinary tool call on a path), but after a compaction only the\n//! session's OWN bound plan is re-injected in full - content parked in an unbound plan\n//! file does not come back. The audit finds every structured mutation the target scope\n//! made to a file that SOME session binds as its plan, and warns when the mutating\n//! session does not bind that file itself.\n//!\n//! Identification is a JOIN against the corpus's `plan_mode` bindings (one scan of\n//! every project, `plan_mode`-prefiltered so it parses almost nothing), never a plans\n//! directory guess (`plansDirectory` is configurable). Bash-side edits are outside\n//! this audit: structured `Write`/`Edit`/`MultiEdit`/`NotebookEdit` only."
}
],
"instrument": "The warning line is longer than previously quoted: `warning: N mutation(s) to <path> by session <id>, which does NOT bind it (bound by <id>, L<line>). Only the BOUND plan is re-injected in full after a compaction.` And the audit must be given explicit project targets to reach the whole corpus (see PLAN-015).",
"located": {
"claude_code": null,
"csift": "0.8.1",
"source": "CHANGELOG 0.8.1; src/plan/audit.rs module doc"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && dirs=$(find . -maxdepth 1 -mindepth 1 -type d -exec basename {} \\; | sed 's/^/@/' | tr '\\n' ' '); csift plan --audit ${=dirs} | rg -i 'warn' ; for f in $(rg -l 'plan_file_reference' --glob '*.jsonl' --max-depth 2 .); do bound=$(csift plan \"$f\" --no-subagents --format json | jq -rc 'select(.kind==\"plan\")|.plan_file'); rg --no-filename 'plan_file_reference' \"$f\" | jq -rc 'select(.attachment.type==\"plan_file_reference\")|.attachment.planFilePath'; done [each compared against $bound] ; python3 walk pairing each `plan_file_reference` record with the nearest preceding `compact_boundary` line ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'plan_file_reference|planContent' | sort | uniq -c",
"observed": "The corpus-scoped audit prints exactly 2 warning lines, matching the JSON summary `warnings:2` and the 2 `bound_by_owner:false` rows: `warning: 31 mutation(s) to ~/.claude/plans/<file> by session <id>, which does NOT bind it (bound by <id>, L1584). Only the BOUND plan is re-injected in full after a compaction.` and the same shape for 262 mutations. Re-injection mechanism: 151 records with `attachment.type==\"plan_file_reference\"` in top-level transcripts, across 13 files / 11 distinct session ids (two sessions each appear under two encoded project dirs). Of those 151, 150 name the transcript's own csift-resolved bound plan and 1 names a foreign plan. 150 of 151 sit 4 to 15 lines after a `compact_boundary` (observed gaps: 4,5,6,7,8,10,15). Each record carries the FULL plan text in `attachment.planContent` - measured payloads of 487001, 493085, 521705 and 553003 characters. Binary 2.1.258 strings: `plan_file_reference` 6, `planContent` 29.",
"rule": "Warnings: one per (owning transcript, plan file) pair with `bound_by_owner:false`, counted from the corpus-scoped audit's warning lines and cross-checked against the JSON summary `warnings` field. Re-injections: one row per `attachment.type==\"plan_file_reference\"` record in a top-level transcript; 'own' means its `planFilePath` string-equals the `plan_file` csift resolves for that same transcript under `--no-subagents`; adjacency counted as (record line number minus nearest preceding `compact_boundary` line number) <= 20.",
"note": "Both halves now rest on instruments rather than on the module doc. csift's warning surface behaves exactly as claimed once scoped correctly - 2 warnings, one per `bound_by_owner:false` pair, JSON summary agreeing. Claude Code's re-injection behavior is confirmed directly and the mechanism is now named: the harness writes a `plan_file_reference` attachment carrying the full `planContent`, and 2.1.258 still ships both strings. The 150/151 count is the load-bearing number - re-injection is keyed to the binding, one file per compaction, so an unbound plan file's content does not come back. The one foreign re-injection is not a counterexample to the hazard: it is the fork instant, where the conversation being continued was the origin's, and it is the same event that gives PLAN-013 its shape."
}
]
},
{
"id": "PLAN-017",
"area": "plan",
"behavior": "Every structured file-mutation tool_use carries its path key as a QUOTED JSON key in the raw line - `\"file_path\"` for Write/Edit/MultiEdit and `\"notebook_path\"` for NotebookEdit - and any quote inside string CONTENT is JSON-escaped, so those two quoted keys are safe raw-byte needles.",
"depends": "The two quoted needles prefilter the TARGET SCOPE only, not the whole corpus. plan --audit runs two different prefilters: line_is_structured_mutation_candidate (the two quoted path keys) over the resolved session files, and line_is_plan_candidate (the single needle b\"plan_mode\", src/plan.rs:83-88) over every project when the scope mutated anything. The needle-mutation run separates them observationally: plan-edit rows went 6 -> 0 while bindings stayed 427.",
"code": [
{
"path": "src/plan/audit.rs",
"lines": "37-40",
"snippet": " static FILE_PATH: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"file_path\\\"\"));\n static NOTEBOOK_PATH: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"notebook_path\\\"\"));"
}
],
"instrument": "The proposed refutation ('replace the needle with an unquoted file_path to see the row count collapse') cannot work: an unquoted needle is a strict SUPERSET of the quoted one, so the row count can only stay equal or grow. The load-bearing test is to substitute a needle that does NOT occur - changing src/plan/audit.rs line 38 to b\"\\\"filepath\\\"\" collapses plan-edit rows 6 -> 0 while the corpus binding count stays 427, which is exactly the silent-zero failure the claim predicts.",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/plan/audit.rs:33-35 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}Write:\\{input:\"file_path\".{0,700}' (2) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\]' | sort | uniq -c (3) python3 scan of every *.jsonl in the csift project's own transcript directory under ~/.claude/projects, counting tool_use blocks named Write/Edit/MultiEdit/NotebookEdit whose raw line contains neither b'\"file_path\"' nor b'\"notebook_path\"' (4) csift plan --audit --format json . (working-tree build), then the same run after editing src/plan/audit.rs line 38 to the non-occurring needle b\"\\\"filepath\\\"\", cargo build, rerun, git checkout -- src/plan/audit.rs, cargo build, rerun",
"observed": "Claude Code 2.1.258 carries its own tool->path-input-key table verbatim: 'Read:{input:\"file_path\",...},Write:{input:\"file_path\",responseMembers:[\"filePath\"]},Edit:{input:\"file_path\",responseMembers:[\"filePath\"]},MultiEdit:{input:\"file_path\",responseMembers:[\"filePath\"]},NotebookEdit:{input:\"notebook_path\",responseMembers:[\"notebook_path\"]},Glob:{input:\"path\",...},Grep:{input:\"path\",...},LSP:{input:\"filePath\",...},Bash:{responseMembers:[\"rawOutputPath\",\"persistedOutputPath\"]},...}'. The file-mutation tool set is the single literal '[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]' (1 occurrence), bound as 'var opr=[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"],LPt=new Set(opr)'. The only other tool carrying a path-shaped input key that is NOT covered by the two needles is LSP (input key 'filePath'), and LSP declares 'isReadOnly(){return!0}', so it is not a mutator. Corpus scan: 23 transcripts, 104821 lines, Edit uses=1722, Write uses=145, MultiEdit uses=0, NotebookEdit uses=0; lines missing both quoted needles = 0 of 1867. Needle-mutation experiment: baseline plan-edit rows=6, bindings=427, plan_files_touched=6; with the needle changed to \"filepath\" plan-edit rows=0, plan_files_touched=0, bindings UNCHANGED at 427; after revert plan-edit rows=6 again.",
"rule": "Binary side: exact literal presence, one hit per distinct string-table entry. Corpus side: one count per tool_use BLOCK whose name is in {Write,Edit,MultiEdit,NotebookEdit}; a block counts as 'missing' when the raw jsonl line carrying it contains neither byte string. Audit side: one plan-edit row per (owning parent session id, mutated path) pair, read from --format json rows with kind==\"plan-edit\"; bindings counted from the summary's 'bindings' field.",
"note": "Behavior holds and is now located in the binary, not just inferred: Claude Code 2.1.258 ships the tool->path-key mapping as a literal table, and the four mutation tools map to exactly the two keys csift prefilters on. The false-positive direction is also safe by JSON construction - content that quotes the key is emitted as \\\"file_path\\\", whose bytes place a backslash where the needle needs its closing quote, so the needle does not match. Two fields needed correction (the refutation instrument, which was backwards, and the depends' claim that the needles prefilter the whole corpus). One standing caveat for a future Claude Code: LSP already uses the camelCase key 'filePath' and is only excluded because it declares isReadOnly; a mutating tool adopting that spelling would fall outside both needles."
}
]
},
{
"id": "PLAN-018",
"area": "plan",
"behavior": "A `plansDirectory` value is resolved AGAINST THE PROJECT ROOT (the session's cwd) and must stay contained in it: Claude Code refuses a value that escapes the root (`../x`, an absolute path elsewhere) with the literal message `plansDirectory must be within project root: ` and falls back to `<claude-home>/plans`.",
"depends": "The harness containment predicate has THREE rejection arms, not two, and csift models only the first: (a) the lexical prefix test n!==e && !n.startsWith(e+separator), which csift mirrors; (b) a filesystem probe over the joined path (an lstat / opendir-nofollow / readlink walker) whose non-undefined result rejects the value outright - unmodeled and not mentioned in the claim; (c) the realpath walk up to the nearest existing ancestor, compared against realpath(root) - the divergence the claim already names. Arms (b) and (c) can only make the harness STRICTER than csift, so csift's lexical mirror is a superset: it can accept a directory the harness refused, never refuse one the harness accepted.",
"code": [
{
"path": "src/plan/slug.rs",
"lines": "39-49",
"snippet": "/// The plans directory, by Claude Code's own rule (binary 2.1.258, re-verified for\n/// v0.10.1): `plansDirectory` is read from the MERGED settings (`<project>/.claude/\n/// settings.local.json` over `<project>/.claude/settings.json` over\n/// `<claude-home>/settings.json`), resolved AGAINST THE PROJECT ROOT (the session's\n/// cwd), and must stay CONTAINED in that root - a value that escapes it (`../x`, an\n/// absolute path elsewhere) is refused and Claude Code falls back to\n/// `<claude-home>/plans`. csift mirrors the lexical containment check (the harness\n/// additionally walks symlinks of the nearest existing ancestor - unmodeled, a\n/// symlinked plans dir that escapes only after resolution is the one divergence).\n/// The transcript's FIRST recorded `cwd`: the harness's project root for the session -\n/// the value the head records are stamped with before any Bash `cd` moves the"
},
{
"path": "src/plan/slug.rs",
"lines": "100-106",
"snippet": " let joined = crate::path::lexical_normalize(&root.join(d));\n let root_n = crate::path::lexical_normalize(root);\n if joined.starts_with(&root_n) {\n joined\n } else {\n default\n }"
}
],
"instrument": "'exactly two literals' needs a counting rule: strings -n 20 | rg 'plansDirectory' returns 5 LINES, of which 2 are standalone string-table entries ('plansDirectoryResolutions' and 'plansDirectory must be within project root: ') and 3 are occurrences inside larger minified-JavaScript chunks (the export map, the settings-schema describe, and the resolver body). Counting rule for 'exactly two': lines matching ^[^{}();=]*plansDirectory[^{}();=]*$.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.1",
"source": "src/plan.rs:198-208 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 20 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,45}plansDirectory.{0,60}' | sort | uniq -c (2) strings -n 20 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}plansDirectory must be within project root.{0,300}' (3) strings -n 20 ~/.local/share/claude/versions/2.1.258 | rg -o 'function le\\(n,e\\)\\{if\\(n!==e.{0,700}' (4) a temporary fixture home passed via --claude-home, holding one transcript whose first record carries a slug and a cwd equal to the fixture project root, run as csift --claude-home <fixture-home> plan @<prefix> --format json, once per plansDirectory value",
"observed": "The resolver is present verbatim: 'class Wtr{#e=void 0;directory(){return this.#e??=this.#n(),this.#e}#n(){let e=Je().plansDirectory;if(e){let i=ne(),r=O(i,e);if(le(r,i))return r;t(`plansDirectory must be within project root: ${e}`,{level:\"error\"})}return F()}reset(){this.#e=void 0}}'. The settings-schema entry states the rule in prose: 'plansDirectory:i().optional().describe(\"Custom directory for plan files, relative to project root. If not set, defaults to ~/.claude/plans/\")'. The containment predicate is 'function le(n,e){if(n!==e&&!n.startsWith(e+B))return!1;if(Cm(ce(),n)!==void 0)return!1;let i=NS(e);if(i===null)return!1;let r=n;for(;;){let s=NS(r);if(s!==null)return s===i||s.startsWith(i+B);let o=A(r);if(o===r)return!1;r=o}}', with 'function NS(t){try{return LW(Dt.native(t))}catch{return null}}'. Fixture results, reading the kind==\"plan\" row's plan_file: no plansDirectory -> <claude-home>/plans/<slug>.md; \"docs/plans\" -> <project-root>/docs/plans/<slug>.md; \"../outside\" -> <claude-home>/plans/<slug>.md; \"/tmp/elsewhere\" -> <claude-home>/plans/<slug>.md; settings.local.json \"local-plans\" over settings.json \"docs/plans\" -> <project-root>/local-plans/<slug>.md; value set only in <claude-home>/settings.json -> still resolved against the PROJECT root; project settings.json over <claude-home>/settings.json -> project value wins.",
"rule": "Binary side: exact literal presence, one line per distinct string-table entry. Fixture side: one observation per plansDirectory value; the observable is the single kind==\"plan\" JSON row's plan_file field; 'contained' means the emitted path is under the fixture project root, 'fell back' means it is under <claude-home>/plans.",
"note": "Behavior confirmed twice over: the harness's own resolver and error string were read out of the binary, and a fixture reproduced all four outcomes (default, contained, dot-dot escape, absolute elsewhere) plus the settings-layer precedence. One fixture result is worth recording because it is not in the claim: a plansDirectory set in <claude-home>/settings.json is still resolved against the PROJECT root, not the config home, so the same user-level value yields a different directory per project. Three fields needed correction: the instrument's literal count, the unreleased status of the csift code under test, and the number of unmodeled arms in the harness's containment check."
}
]
},
{
"id": "PLAN-019",
"area": "plan",
"behavior": "The five layers and the high-to-low order policySettings > flagSettings > localSettings > projectSettings > userSettings are correct, but the supporting literal is not the merge order. The authoritative literal is the LOW-to-HIGH list [\"userSettings\",\"projectSettings\",\"localSettings\",\"flagSettings\",\"policySettings\"] (bound as Bs and returned by nn()), which the binary itself annotates 'Ordered low-to-high priority - later entries override earlier ones.' plansDirectory is read off the merged result of that list.",
"depends": "The gap is wider than 'three of five'. Beyond the missing policy and flag layers, the two layers csift does read can resolve to a DIFFERENT DIRECTORY than csift assumes: projectSettings resolves to the record's cwd (which csift matches), but localSettings resolves via eW(cwd, canonicalGitRoot), which prefers the canonical GIT ROOT when it differs from the cwd, is not the home directory, and passes an ownership probe. So for a session whose cwd is a subdirectory of its git repository, the harness reads <git-root>/.claude/settings.local.json while csift reads <cwd>/.claude/settings.local.json.",
"code": [
{
"path": "src/plan/slug.rs",
"lines": "82-85",
"snippet": " let mut candidates: Vec<PathBuf> = vec![home.join(\"settings.json\")];\n candidates.push(root.join(\".claude\").join(\"settings.json\"));\n candidates.push(root.join(\".claude\").join(\"settings.local.json\"));\n let mut value: Option<String> = None;"
},
{
"path": "src/plan/slug.rs",
"lines": "93-95",
"snippet": " if let Some(d) = v.get(\"plansDirectory\").and_then(serde_json::Value::as_str) {\n value = Some(d.to_string()); // later (more specific) scopes win\n }"
}
],
"instrument": "The cited literal ['policySettings','flagSettings',...['localSettings','projectSettings']...] exists, but it is a first-wins precedence walk for a DIFFERENT settings key (an auto-memory directory lookup: 'for(let r of n){let o=be(r)?.autoMemoryDirectory;if(o!=null)return{dir:o,source:r}}'), and its middle pair is behind a runtime conditional. Five distinct policySettings-led arrays exist, two of them in orders that contradict each other, so no single bracketed literal settles precedence. Grep for the low-to-high five-element list and its describe string instead.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 12 ~/.local/share/claude/versions/2.1.258 | grep -oE '\"(userSettings|projectSettings|localSettings|policySettings|managedSettings|flagSettings)\"' | sort | uniq -c | sort -rn (2) strings -n 12 ~/.local/share/claude/versions/2.1.258 | grep -oE '\\[\"policySettings\",\"[^]]{0,160}\\]' | sort -u (3) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}\"userSettings\",\"projectSettings\",\"localSettings\",\"flagSettings\",\"policySettings\".{0,400}' (4) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,300}case\"localSettings\".{0,300}' (5) strings -n 8 ~/.local/share/claude/versions/2.1.258 | rg -o 'managed-settings\\.json|settings\\.local\\.json' | sort | uniq -c",
"observed": "Layer-literal counts reproduce the claim exactly: userSettings 267, policySettings 209, localSettings 191, projectSettings 148, flagSettings 114, managedSettings 4. The canonical layer list is 'var Bs=[\"userSettings\",\"projectSettings\",\"localSettings\",\"flagSettings\",\"policySettings\"]', repeated as 'function nn(){return[\"userSettings\",\"projectSettings\",\"localSettings\",\"flagSettings\",\"policySettings\"]}', and the binary states its own ordering: the same five-element enum is described as 'Ordered low-to-high priority \\u2014 later entries override earlier ones.' Corroborated by the reverse walk 'function nE(e){let n=Ci();for(let r=n.length-1;r>=0;r--){let o=n[r];if(be(o)?.[e]!==void 0)return o}return null}'. plansDirectory is read from the MERGED object: 'function Je(){return Rb().settings||{}}' and the resolver's 'let e=Je().plansDirectory'. Human names: 'case\"userSettings\":return\"user settings\";case\"projectSettings\":return\"shared project settings\";case\"localSettings\":return\"project local settings\";case\"flagSettings\":return\"command line arguments\";case\"policySettings\":return\"enterprise managed settings\"'. Filenames: settings.local.json 144 occurrences, managed-settings.json 36. Per-layer directory resolution: 'function WCt(e,n){switch(e){case\"userSettings\":return ue(Se());case\"policySettings\":case\"projectSettings\":return ue(n.cwd);case\"localSettings\":return eW(n.cwd,n.canonicalGitRoot);case\"flagSettings\":return n.flagPath?hg(ue(n.flagPath)):ue(n.cwd)}}'. The four precedence arrays found by (2) are ['policySettings','flagSettings',...conditional ['localSettings','projectSettings']...,'userSettings'], ['policySettings','flagSettings','userSettings'], ['policySettings','flagSettings'], ['policySettings','projectSettings','localSettings'] and ['policySettings','userSettings','flagSettings'] - five distinct literals, not one.",
"rule": "One occurrence per quoted literal in the binary's string table, counted with grep -oE over strings -n 12 output and tallied by uniq -c. A 'precedence array' is any bracketed literal whose first element is \"policySettings\".",
"note": "Substance holds - five layers, that precedence, localSettings as .claude/settings.local.json, the managed layer as managed-settings.json - and every count in the claim's instrument reproduced to the digit. Three fields needed correction: the cited array is a different key's lookup rather than the merge order, the real ordering literal is the low-to-high list that documents itself, and the depends understates the gap by omitting the git-root resolution of localSettings. Note also that managedSettings (4 hits) is not a sixth layer: the managed layer is named policySettings and only its FILE is called managed-settings.json."
}
]
},
{
"id": "PLAN-020",
"area": "plan",
"behavior": "'--settings' is one of THREE settings-affecting launch flags, all read in the same eager-load sequence: --settings supplies the flagSettings layer, --managed-settings supplies the policy layer's file, and --setting-sources selects which sources load at all (with --restricted equivalent to an empty source list). Precedence of the flagSettings layer is as claimed - above localSettings, projectSettings and userSettings - but BELOW policySettings, per the low-to-high layer list.",
"depends": "Confirmed and widened. csift's blindness is not only to --settings: --managed-settings and --setting-sources/--restricted change which settings apply too, and --setting-sources can DISABLE a source csift does read (the harness carries the message 'source is disabled for this session (--setting-sources)'), so csift can be wrong by reading a file the session ignored as well as by missing one it honored. The blindness is now instrument-backed rather than assumed: the session registry files under ~/.claude/sessions record no argv - their key union across 7 files contains nothing settings- or argument-related - and no transcript record type carries launch flags either.",
"code": [
{
"path": "src/plan/slug.rs",
"lines": "86-92",
"snippet": " for p in candidates {\n let Ok(raw) = std::fs::read_to_string(&p) else {\n continue;\n };\n let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {\n continue;\n };"
},
{
"path": "src/plan/slug.rs",
"lines": "82-85",
"snippet": " let mut candidates: Vec<PathBuf> = vec![home.join(\"settings.json\")];\n candidates.push(root.join(\".claude\").join(\"settings.json\"));\n candidates.push(root.join(\".claude\").join(\"settings.local.json\"));\n let mut value: Option<String> = None;"
}
],
"instrument": "sort -u over the extracted spellings yields 13 distinct hits at CC 2.1.258, not the 4 listed. The two the claim names as error strings are both present. For a stable rerun, prefer the argv-table literal '\"--settings\":(e)=>{t.settings=e}' and the flagSettings->'--settings' mapping, which pin the flag's identity rather than counting prose.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -n 10 ~/.local/share/claude/versions/2.1.258 | grep -oE '\\-\\-settings[ <]?[a-z-]*' | sort -u (2) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}\"--settings.{0,300}' (3) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,90}--settings cannot.{0,160}' (4) python3 over every ~/.claude/sessions/*.json, printing the union of top-level keys",
"observed": "The flag is parsed with a value: the argv table contains 'a={\"--cwd\":(e)=>{r=e},\"--settings\":(e)=>{t.settings=e},\"--add-dir\":(e)=>t.addDir.push(e),...}', and the eager settings load reads three settings flags in sequence: 'OVt(\"--settings\");if(e)Ypr(e);let a=OVt(\"--managed-settings\");if(a)c(a);let n=OVt(\"--setting-sources\");if(n!==void 0)g(n);if(LR(\"--restricted\")||PW())g(\"\"),gnt(!0);'. The flagSettings layer is mapped to the flag by name in a permission-warning path: 'nt.source===\"flagSettings\"&&!Ho(\"flagSettings\")?\"--settings\":nt.source===\"policySettings\"?\"managed policy settings\":...', and flagSettings' human name is 'command line arguments'. Error strings present verbatim: 'hooks given with --settings cannot be told apart from ones added at run time' and 'All hooks are switched off in your user settings or --settings file (disableAllHooks)'. Also present: 'source is disabled for this session (--setting-sources)'. Distinct --settings spellings found by (1): 13, not 4 - --settings, '--settings ', '--settings and', '--settings cannot', '--settings file', '--settings files', '--settings flag', '--settings or', '--settings sources', '--settings still', '--settings to', '--settings value', '--settings-enabled'. Session registry: 7 files parsed under ~/.claude/sessions, union of top-level keys = [bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version] - no argv, args, flags, settings or setting-source key of any kind.",
"rule": "Binary side: distinct string-table hits containing the flag spelling, deduplicated with sort -u, one row per distinct extracted substring. Registry side: one file per *.json under ~/.claude/sessions (the paired *.key files are not JSON and are excluded); the observable is the UNION of top-level keys across all parsed files.",
"note": "The flag exists, takes a value, is the flagSettings layer, and outranks the project, local and user layers - all confirmed from the binary. Two fields needed correction: the string set is three times larger than listed, and the flag is one of three settings-affecting launch flags rather than the only one. The depends is now positively verified rather than inferred, because the session registry was inspected and demonstrably records no launch arguments; that also means csift cannot even DISCLOSE the uncertainty per session, since there is no on-disk signal that a settings flag was used."
}
]
},
{
"id": "QT-001",
"area": "queue-and-telemetry",
"behavior": "Claude Code writes a `type:\"queue-operation\"` line for every input-queue state change. The line carries four to six top-level keys - always `type`, `operation`, `timestamp` and `sessionId`, optionally `content` and `reason` - and no `uuid`, no `parentUuid`, no `message`, no `promptId` and no queue id. The measured `operation` vocabulary is `enqueue`, `dequeue`, `remove` and `popAll`, treated as an open set.",
"depends": "csift promotes the line to the LLM-invisible leaf `user.queued` and carries `operation` and `reason` verbatim into the label zone and the JSON hit; with no `uuid` on the line the record is addressable only by `show --line`, and with no `message{}` a role-based byte prefilter never keeps it, which is why the leaf needs its own explicit gate.",
"code": [
{
"path": "src/model/record.rs",
"lines": "155-159",
"snippet": " /// `queue-operation` (v0.10.0): the queue event - `enqueue` | `dequeue` | `remove` |\n /// `popAll` (open set; measured those four). The human-typed (or automation) text\n /// rides top-level `content` on every operation except `dequeue`.\n #[serde(default)]\n pub operation: Option<String>,"
}
],
"instrument": "`rg -NI '\"type\":\"queue-operation\"' ~/.claude/projects -g '*.jsonl'` piped through a JSON reader that unions all top-level keys; counting rule = one observation per physical line, reported as the distinct sorted key sets and their frequencies (measured 13,412 lines, exactly three key sets). `csift search '' <project-dir> -t user.queued --count-by label` gives the labelled subset.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md section 5.1; src/model/record.rs:148-152 comment; CHANGELOG 0.10.0 (queue facts, measured); SPEC.md section 6 v0.10.0 ledger item 7; SPEC.md v0.10.0 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'type:\"queue-operation\"[^}]{0,300}' AND for each of the 15 project directories separately: rg -NI --no-heading '\"type\":\"queue-operation\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (json.loads each line, union of sorted top-level key sets)",
"observed": "Binary writer, verbatim: `function o(Br,br,Ts){let qi={type:\"queue-operation\",operation:Br,timestamp:new Date().toISOString(),sessionId:Q(),...br!==void 0&&{content:br},...Ts!==void 0&&{reason:Ts}};n(qi)}`. Corpus: 13,512 queue-operation lines, EXACTLY three distinct key sets - 9,130 x [content, operation, sessionId, timestamp, type]; 4,237 x [operation, sessionId, timestamp, type]; 145 x [content, operation, reason, sessionId, timestamp, type]. No uuid, parentUuid, message, promptId or queue id in any set. operation values on disk: enqueue 6,760 / dequeue 3,441 / remove 3,203 / popAll 108. Binary call sites of the same recorder: o(\"enqueue\"), o(\"dequeue\"), o(\"remove\"), o(\"popAll\",typeof ...), and o(\"popOne\",typeof Eo.value===\"string\"?Eo.value:void 0).",
"rule": "One observation per physical jsonl line whose parsed top-level type == 'queue-operation'; key sets are the sorted tuple of top-level keys; rg is run once per project directory and the per-directory results are summed (15 directories, 5.2 GB total). Binary side: one observation per matched string literal.",
"note": "Behavior statement confirmed twice over: the binary writer literally builds the 4-to-6-key object, and the corpus shows exactly the three key sets that writer can produce. Only the instrument's line count and the operation vocabulary needed updating."
}
]
},
{
"id": "QT-002",
"area": "queue-and-telemetry",
"behavior": "Whether a `queue-operation` line carries the queued text at all is OPERATION-DEPENDENT: top-level `content` is present on `enqueue` 6,710/6,710 (100%), `popAll` 108/108 (100%), `remove` 2,372/3,168 (75%) and `dequeue` 0/3,426 (never). A `dequeue` therefore carries nothing to search.",
"depends": "csift assigns no label to a content-less queue line and renders the label zone as `[enqueue]` or `[remove - <reason>]`; a `dequeue` can be paired with its `enqueue` only by ordering and text, never by an id.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "38-42",
"snippet": " fn queued_class(&self) -> Option<Class> {\n let text = self.content_str()?;\n if text.trim().is_empty() {\n return None;\n }"
}
],
"instrument": "Extract every `queue-operation` line and cross-tabulate `operation` against the presence of `content` and of `reason`; counting rule = one row per physical queue line, grouped by operation value. Expect 100% content on enqueue and popAll, 0% on dequeue.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 6 v0.10.0 ledger item 7; src/model/record.rs:148-152 comment; CHANGELOG 0.10.0 (queue facts, measured)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each project directory separately: rg -NI --no-heading '\"type\":\"queue-operation\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (cross-tabulate parsed `operation` against presence of top-level `content` and `reason`)",
"observed": "enqueue: content 6,760/6,760 (100%) - popAll: content 108/108 (100%) - remove: content 2,407/3,203 (75.1%) - dequeue: content 0/3,441 (0%). Rendered form checked with `csift show @<SESSION> --line <N>`: a content-bearing remove renders `user.queued [remove - absorbed_mid_turn]`; a content-bearing enqueue renders `user.queued [enqueue]`. A content-less or rider queue line is not addressable at all: `csift show --line` on one returns `csift: error: no such record(s): L<N>` and names which line kinds an explicit address does render.",
"rule": "One row per physical queue-operation line, grouped by the `operation` value; 'content present' = the top-level key exists (an empty string would still count, none were observed).",
"note": "The operation-dependence is exact: every enqueue and every popAll carried text, no dequeue ever did. The 75% figure for remove reproduced to within 0.1 percentage point."
}
]
},
{
"id": "QT-003",
"area": "queue-and-telemetry",
"behavior": "A `reason` field occurs only on a `remove` queue line and takes exactly two measured values, `absorbed_mid_turn` (99 occurrences) and `delivered_to_agent` (11) - both structural evidence that the queued text was consumed. The field is absent on every other operation and every other line type.",
"depends": "A queued hit renders `[remove - <reason>]` in the label zone and carries JSON `queue_reason`, kept verbatim as an open set; the two values are the only consumption evidence the format offers, and csift reports them rather than deriving a dispatch verdict from them.",
"code": [
{
"path": "src/model/record.rs",
"lines": "161-165",
"snippet": " /// `queue-operation` `remove` reason - measured values `absorbed_mid_turn` and\n /// `delivered_to_agent`, both STRUCTURAL evidence that the queued text was\n /// consumed. Absent on every other line. Open set, kept verbatim.\n #[serde(default)]\n pub reason: Option<String>,"
}
],
"instrument": "`csift search '' <target> -t user.queued --format json | jq -r '.queue_operation + \"/\" + (.queue_reason // \"-\")' | sort | uniq -c`; counting rule = one row per content-bearing queue line. Cross-check the raw side with `rg -o '\"reason\":\"[a-z_]+\"' over the same files.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "src/model/record.rs:154-158 comment; dev session 2026-09-02; SPEC.md section 5.1; CHANGELOG 0.10.0 (the queue line has no join key); SPEC.md v0.10.0 ledger; SPEC.md section 6 v0.10.0 ledger item 7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each project directory separately: rg -NI --no-heading '\"reason\":' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (keep lines whose PARSED object has a top-level `reason`, bucket by (type, operation) and by value); plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'consume\\([A-Za-z0-9_.]+,\\{reason:\"[a-z_]+\"\\}' and rg -oN '.{200}o\\(\"remove\".{200}'",
"observed": "Corpus: top-level `reason` occurs on 145 lines, all of them type=queue-operation with operation=remove; values absorbed_mid_turn 133, delivered_to_agent 12. In a second, larger project directory 786 lines contain the substring '\"reason\":' and ZERO of them carry it as a top-level key (all nested). Binary: the remove helper is `function Dr(Br,br,Ts){...o(\"remove\",typeof _o.value===\"string\"?_o.value:void 0,Ts)}` - i.e. the third argument is the recorded reason - and the reason literals reaching it include `absorbed_mid_turn`, `delivered_to_agent`, `delivered_as_tool_result`, `dropped_by_hook` and `cleared_on_cancel`.",
"rule": "One observation per physical line whose parsed object has `reason` as a TOP-LEVEL key (nested `reason` keys inside toolUseResult and message payloads are excluded); values counted verbatim.",
"note": "The 'open set, kept verbatim' discipline is vindicated - and the binary shows three unobserved values, two of which mean the OPPOSITE of consumption. That asymmetry is the sharpest thing this check found and deserves its own claim."
}
]
},
{
"id": "QT-004",
"area": "queue-and-telemetry",
"behavior": "A `queue-operation` line carries NO join key to the user record its text eventually becomes: the measured 4-6 keys include no `promptId`, no `uuid` and no queue id. Dispatch is therefore unprovable from the queue side - the delivered record can be reached only by ordering and by the text itself.",
"depends": "csift renders the verbatim `operation` and `reason` facts and NEVER asserts a `dispatched` flag; inventing a join would fabricate a link between a queued draft and a delivered turn that the format does not carry.",
"code": [
{
"path": "src/search/hits.rs",
"lines": "189-199",
"snippet": " // v0.10.0: the queue facts ride only a queue-operation record (None elsewhere).\n let queue_operation = if rec.is_type(\"queue-operation\") {\n rec.operation.clone()\n } else {\n None\n };\n let queue_reason = if rec.is_type(\"queue-operation\") {\n rec.reason.clone()\n } else {\n None\n };"
},
{
"path": "src/search/render.rs",
"lines": "47-55",
"snippet": " // v0.10.0: a queued line names its queue event (and a remove's reason) in the label\n // zone - display-only; the matchable text stays the verbatim queued content.\n if h.class == Class::UserQueued {\n let op = h.queue_operation.as_deref().unwrap_or(\"queued\");\n return match h.queue_reason.as_deref() {\n Some(reason) => format!(\"{} [{op} · {reason}]\", h.class.path()),\n None => format!(\"{} [{op}]\", h.class.path()),\n };\n }"
},
{
"path": "src/model/taxonomy.rs",
"lines": "55-60",
"snippet": " /// (no `message{}`); the dispatched twin, when there is one, is the later\n /// `user.message` record. Automation riders (a `<task-notification>` / peer\n /// message queued by the harness) are NOT the human and carry no label here;\n /// content-less `dequeue` lines carry nothing to search. The queue line has no\n /// join key (measured: 4-6 keys, no promptId/uuid), so `dispatched` is never\n /// asserted - only the verbatim `operation` + `reason` facts ride the hit."
}
],
"instrument": "`csift show <target> --line <a queue-operation line> --raw | jq 'keys'`: no promptId, uuid or queue-id field appears. Counting rule = the key set of one queue line, repeated over the corpus-wide union (three distinct key sets, none with a join field).",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "CHANGELOG 0.10.0 (the queue line has no join key); SPEC.md section 5.1; SPEC.md section 6 v0.10.0 ledger item 7; src/model/record.rs:154-158 comment; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift show @<SESSION> --line <N> --raw | jq -c 'keys' (run on two queue-operation lines: one remove-with-reason, one enqueue); plus the corpus-wide key-set union from QT-001; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'type:\"queue-operation\"[^}]{0,300}'",
"observed": "Raw keys of a remove line: [\"content\",\"operation\",\"reason\",\"sessionId\",\"timestamp\",\"type\"]. Raw keys of an enqueue line: [\"content\",\"operation\",\"sessionId\",\"timestamp\",\"type\"]. Corpus-wide union over 13,512 lines: exactly three key sets, none containing promptId, uuid, parentUuid, messageId or any queue/task id. Binary writer builds the object from exactly {type, operation, timestamp, sessionId, content?, reason?} - there is no join field to omit.",
"rule": "The key set of one queue line via `jq keys`, generalised by the corpus-wide union of parsed top-level key sets (one observation per physical line); a 'join key' means any field naming the eventual user record (promptId, uuid, messageId) or the queue entry (a queue/entry id).",
"note": "Confirmed at the source: the writer in the shipped binary has no join field to write, so dispatch is unprovable from the queue side by construction and not merely unobserved. csift refusing to assert `dispatched` is correct. Only the record.rs line numbers moved."
}
]
},
{
"id": "QT-005",
"area": "queue-and-telemetry",
"behavior": "The input queue is dominated by automation rather than human text: of 9,190 content-bearing `queue-operation` lines the content-leading shape was `<task-notification>` 7,996, plain prose 1,141, a slash command 24 and `<agent-message from=\"...\">` about 29 - roughly 87% harness RIDERS, whose delivered twin already classifies as a harness notification or an inbound peer message.",
"depends": "csift labels only a human-typed queue line `user.queued`, refusing a rider by the same content-shape law that reparents the delivered user record; labelling every content-bearing queue line would inflate the apparent human queue traffic roughly sevenfold and duplicate records already labelled elsewhere.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "32-48",
"snippet": " /// `user.queued` iff the queue line carries the HUMAN's text. The queue also\n /// carries harness riders - a `<task-notification>` pulse or a peer message - whose\n /// delivered twin already classifies `harness.notification.*` / `agent.communication\n /// .inbox`; the same content-shape law that reparents those user records applies,\n /// so a rider is never the human here. A content-less line (`dequeue`) has nothing\n /// to search and carries no label.\n fn queued_class(&self) -> Option<Class> {\n let text = self.content_str()?;\n if text.trim().is_empty() {\n return None;\n }\n let at_boundary = text.trim_start();\n if at_boundary.starts_with(TASK_NOTIFICATION_PREFIX) || is_peer_message(text) {\n return None;\n }\n Some(Class::UserQueued)\n }"
}
],
"instrument": "Extract content-bearing `queue-operation` lines and bucket them by the first non-whitespace token of `content`; counting rule = one bucket observation per content-bearing line (riders = a boundary-anchored notification or peer tag). Then `csift search '' <target> -t user.queued -c` must be far smaller than that denominator.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 5.1; AGENTS.md section 3.3a; SPEC.md section 6 v0.10.0 ledger item 7; AGENTS.md section 3.3a (queued rider law); CHANGELOG 0.10.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each project directory separately: rg -NI --no-heading '\"type\":\"queue-operation\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (bucket by the first non-whitespace token of `content`); then csift search '' -t user.queued --count-by label --max-count 0 (whole corpus, csift 0.10.0)",
"observed": "9,275 content-bearing queue lines: <task-notification> 8,075 (87.0%), plain prose 1,146 (12.4%), <agent-message ...> 30, a bare slash command 24. Riders (task-notification + agent-message + teammate-message) = 8,105 = 87.4%. csift's corpus-wide `user.queued` census = 1,170 records, which equals 1,146 + 24 exactly - i.e. csift labels every non-rider content-bearing line and no rider. In one project directory taken alone the split was 685 riders / 62 human-shaped and csift's census for that directory was 62.",
"rule": "One bucket observation per content-bearing queue line, bucketed by the first non-whitespace token; 'rider' = content begins with <task-notification, <agent-message or <teammate-message. csift census counts one record per labelled record.",
"note": "The rider law is exact, not approximate: csift's independent census (1,170) equals the hand-computed non-rider count (1,170) to the record. 'roughly sevenfold' measures 7.9x."
}
]
},
{
"id": "QT-006",
"area": "queue-and-telemetry",
"behavior": "The human-versus-automation discriminator lives on the DELIVERED twin, never on the queue line: the `queued_command` attachment carries `commandMode` in {`task-notification` 2,106, `prompt` 226}, and the delivered user record carries `origin.kind` in {`task-notification` 2,804, `human` 2,482, `peer` 57, `coordinator` 49} plus `promptSource` in {`system` 2,861, `typed` 2,564, `queued` 104}. `promptSource` alone is NOT a draft discriminator: measured drafts were 315 `typed` against 2 `queued` while finals were 317/317 `typed`.",
"depends": "csift does not model `origin` or `promptSource` today and classifies by content shape instead; these are the structural fields any future authorship or dispatch assertion would have to key on, and the draft census is why `promptSource` cannot stand in for the superseded-draft rule.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "32-35",
"snippet": " /// `user.queued` iff the queue line carries the HUMAN's text. The queue also\n /// carries harness riders - a `<task-notification>` pulse or a peer message - whose\n /// delivered twin already classifies `harness.notification.*` / `agent.communication\n /// .inbox`; the same content-shape law that reparents those user records applies,"
}
],
"instrument": "`rg -oNI '\"promptSource\":\"[a-z]+\"' ~/.claude/projects -g '*.jsonl' | sort | uniq -c`, and the same for `\"commandMode\":\"...\"` and for `\"kind\":\"...\"` inside an `origin` object; counting rule = one observation per match. `promptSource:\"queued\"` was observed spanning Claude Code 2.1.177 to 2.1.252.",
"located": {
"claude_code": "2.1.252",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md section 6 v0.10.0 ledger item 7"
},
"first_seen_claude_code": "2.1.177",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each project directory separately: rg -NI --no-heading '\"promptSource\"' / 'commandMode' / '\"origin\":{\"kind\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (parse each line, census the parsed values by carrier); draft side: csift search '' -t user.unsent --raw --max-count 0 (whole corpus) then census `promptSource` on those records, and join their `parentUuid` back to non-draft string-content user records via rg -F -f <uuid list> per project directory; binary side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,120}promptSource:.{0,200}' and rg -oN 'function lle\\(.{0,900}'",
"observed": "promptSource on the corpus has FIVE values, not three: system 2,874, typed 2,607, queued 105, suggestion_accepted 1, sdk 1 - and the binary's own switch enumerates exactly those five (`case\"sdk\" ... case\"system\" ... case\"typed\":case\"queued\":case\"suggestion_accepted\"`). commandMode inside a queued_command attachment: task-notification 2,131, prompt 228 (two values). origin.kind on the delivered type:\"user\" record: task-notification 2,819, human 2,388, peer 51, coordinator 49; origin also rides inside the queued_command attachment (human 140, peer 6). The binary's origin classifier switches on ELEVEN kinds: human, plugin, channel, task-notification, peer, coordinator, unclassified, observer, auto-continuation, observer-activity, slack-ping. promptSource:\"queued\" spans 2.1.177 through 2.1.258. Draft census: 827 superseded-draft records - typed 616, queued 13, field absent 198. Their surviving siblings (non-draft string-content user records sharing a draft parentUuid): 446 records - typed 321, system 11, queued 0, field absent 114.",
"rule": "One observation per parsed line for each field; draft set = every record csift labels `user.unsent`; 'final' = a non-draft `type:\"user\"` record with STRING message.content whose parentUuid is shared with at least one draft. Binary vocabularies = one observation per switch-case literal.",
"note": "The core assertion - the discriminator lives on the delivered twin, and promptSource alone cannot separate draft from final - survived every instrument. Both enumerations needed widening, and the binary now supplies the closed sets rather than leaving them as corpus observations."
}
]
},
{
"id": "QT-007",
"area": "queue-and-telemetry",
"behavior": "Human prose enqueued into the input queue does not reliably become a user record: counting enqueue lines only, over 6 sessions (n=782 texts, whitespace-normalized), 72% match a later user record exactly and 81% match by prefix - so 19-28% of typed queue text never becomes a user record at all. An earlier ~61%-never figure counted every queue operation, remove lines included, over 3 sessions and is superseded by the narrower count.",
"depends": "the gap is why `user.queued` exists as its own searchable leaf: for that 19-28% the queue line is the only on-disk copy of what the operator typed, and a `-t user.message` search over it would report a definitive absence.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "41-47",
"snippet": " return None;\n }\n let at_boundary = text.trim_start();\n if at_boundary.starts_with(TASK_NOTIFICATION_PREFIX) || is_peer_message(text) {\n return None;\n }\n Some(Class::UserQueued)"
}
],
"instrument": "For one session extract every `enqueue` line and test its `content` against `csift search -F <text> @<id> -t user.message`; counting rule = enqueue lines only (never remove or dequeue), one comparison per enqueued text, with exact-string and 40-char-prefix matches counted separately.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md v0.10.0 ledger; SPEC.md section 6 v0.10.0 ledger item 7; CHANGELOG 0.10.0 (queue facts, measured)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over the SIX session files carrying the most enqueue lines corpus-wide (found with rg -c --no-heading '\"operation\":\"enqueue\"' -g '*.jsonl' ~/.claude/projects/<PROJ> per directory): read each file once, collect every enqueue `content` and every `type:\"user\"` record text (string content and text blocks), whitespace-normalize both, then test exact membership, 40-character prefix membership and substring membership",
"observed": "6 session files; 5,721 enqueue lines total, of which 777 are human-shaped (non-rider). Human-shaped: exact match to a later user text 565 (73%), 40-char-prefix match 593 (76%), substring of some user text 638 (82%). So 18-24% of human-typed enqueued text never becomes a user record. Rider enqueues for contrast: 4,944 lines, 51% exact, 63% prefix40.",
"rule": "One comparison per enqueue line (never a remove or dequeue), restricted to human-shaped content (not <task-notification / <agent-message / <teammate-message). Texts whitespace-normalized (runs of whitespace collapsed to one space, trimmed) on both sides. 'exact' = normalized equality; 'prefix40' = some user text starts with the enqueue's first 40 characters; 'substring' = the enqueue text occurs inside the newline-joined user texts.",
"note": "The gap is real and reproduced closely (73% vs the claimed 72% exact; the 19-28%-never band brackets the measured 18-24%). n came out 777 against the claimed 782 on a differently-selected session set, which is close enough to be the same effect. The one thing that needs fixing is naming the match rule."
}
]
},
{
"id": "QT-008",
"area": "queue-and-telemetry",
"behavior": "A background task completion notification is delivered on THREE carriers: a `queue-operation` line with `operation:\"enqueue\"`, an `attachment` whose payload `type` is `queued_command` (carrying `commandMode:\"task-notification\"`), and - only when the session was idle - a `type:\"user\"` string record. 1,000 of 2,502 returned main-lane shells (40%) never produce a user record at all, so for those the queue line and the attachment are the only trace.",
"depends": "the csift background scanner reads all three carrier shapes; restricting it to `type:\"user\"` records would miss 40% of shell completions and leave them permanently `open`, inflating the idle-background-open verdict.",
"code": [
{
"path": "src/live/background_scan.rs",
"lines": "225-236",
"snippet": "pub(crate) fn carrier_text(rec: &Record) -> Option<String> {\n if rec.is_type(\"queue-operation\") {\n rec.content_str().map(str::to_string)\n } else if rec.attachment_type().as_deref() == Some(\"queued_command\") {\n rec.attachment_value()\n .and_then(|v| v.get(\"prompt\")?.as_str().map(str::to_string))\n } else if let Some(Content::Text(s)) = rec.message.as_ref().and_then(|m| m.content.as_ref()) {\n Some(s.clone())\n } else {\n None\n }\n}"
},
{
"path": "src/live/background.rs",
"lines": "22-27",
"snippet": "//! It rides THREE carriers: a `type:\"user\"` string record when the session was idle,\n//! or (40% of returned shells) a `queue-operation` enqueue + remove and a\n//! `queued_command` attachment when it landed mid-turn - never a user record. A shell\n//! launched from a SUBAGENT lane is completed in the PARENT main transcript (607/618\n//! measured; zero notifications exist in any subagent transcript), so this scan reads\n//! launches from every lane and completions from the main file."
}
],
"instrument": "Take a known background task id from a launch result and `rg -n '<its id>' <the main transcript>`, then classify each matching line by its `type` field; counting rule = one carrier observation per matching line, bucketed into `queue-operation` / `attachment` / `user`. A launch with a queue row but no user row is a mid-turn delivery.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each project directory separately: rg --no-heading -H -N -e run_in_background -e tool-use-id -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 - collect main-lane assistant tool_use blocks (Bash/PowerShell) with input.run_in_background == true as LAUNCHES, then join every <tool-use-id> found in a queue-operation `content`, a `queued_command` attachment `prompt`, or a `type:\"user\"` string content back to that launch id",
"observed": "2,534 main-lane background shell launches; 2,520 have at least one completion carrier. Carrier-set distribution: 1,193 {queue-operation, user(string), +hook_success attachment}, 1,015 {queue-operation, attachment:queued_command}, 311 {queue-operation, user(string)}, 1 {queue-operation}. Returned shells with NO user-record carrier: 1,016 = 40%. The queue-operation carrier is present on all 2,520. The `queued_command` attachment appears on exactly the 1,015 launches that have no user record - i.e. mid-turn delivery. No <task-notification> element was found in any subagent transcript; the 9 subagent user-string records matching <tool-use-id> all begin `[SYSTEM NOTIFICATION - NOT USE...` and the 171 other subagent occurrences are inside tool_result payloads.",
"rule": "One launch observation per assistant tool_use block with run_in_background true in a non-subagent transcript; one carrier observation per (launch id, carrier kind) pair, joined on the exact <tool-use-id> string. 'returned' = at least one carrier; 'no user record' = no carrier of kind type:\"user\" with string content.",
"note": "The strongest result in this batch: the three-carrier model is not just observed, it partitions cleanly - a queue-operation on 100% of returned shells, a queued_command attachment on exactly the 1,015 with no user record, a user string record on the other 1,504. Restricting the scan to user records would indeed lose 40% of shell completions."
}
]
},
{
"id": "QT-009",
"area": "queue-and-telemetry",
"behavior": "Claude Code writes a `type:\"system\"` / `subtype:\"turn_duration\"` record per assistant turn, not per user record (4,580 such records against roughly 40,000 user records in the same corpus). Its payload is integers only - `durationMs` (median 134,688 ms) and `messageCount` (a running total) - and unlike the metadata cache lines it DOES carry `uuid`, `timestamp` and `parentUuid` with `isMeta:false`, plus `slug` on about 93% of records and `sessionKind` rarely.",
"depends": "csift promotes it to the gated leaf `harness.meta.turn-duration`, renders a FABRICATED `[turn duration: ...]` key=value excerpt from exactly these fields, and - because the record carries a uuid and a timestamp - lets `show --uuid` and `show --line` address it flag-free.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "191-196",
"snippet": " Class::MetaTurnDuration => {\n let mut fields: Vec<String> = Vec::new();\n let ms = Record::u64_field(rec.duration_ms.as_ref());\n if let Some(ms) = ms {\n fields.push(format!(\"{} · durationMs={ms}\", fmt_ms(ms)));\n }"
},
{
"path": "src/search/matcher.rs",
"lines": "506-508",
"snippet": " if args.reaches_gated(Class::MetaTurnDuration) {\n verifiable.push(b\"turn_duration\");\n }"
},
{
"path": "src/model/classify_promoted.rs",
"lines": "19",
"snippet": "\"stop_hook_summary\" => Some(Class::MetaStopHooks),"
}
],
"instrument": "`rg -NI '\"subtype\":\"turn_duration\"' ~/.claude/projects -g '*.jsonl'` into a JSON reader; counting rule = one observation per physical line, unioning keys and computing a per-key presence rate. Then `csift search '' <project-dir> -t harness.meta.turn-duration --count-by label` must equal the line count, and `csift show <target> --uuid <a turn_duration uuid>` must render the record.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 5.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each project directory separately: rg -NI --no-heading '\"subtype\":\"turn_duration\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (per-key presence rate, distinct key sets, durationMs median); denominators via rg -c --no-heading '\"type\":\"user\"' / '\"type\":\"assistant\"' / '\"stop_reason\":\"end_turn\"' per directory, split main-lane vs subagent by whether the path contains /subagents/; csift side: csift search '' @<PROJ> -t harness.meta.turn-duration --count-by label and csift show @<SESSION> --uuid <uuid>; binary side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,200}\"turn_duration\".{0,400}'",
"observed": "4,633 turn_duration records, ALL type=system, ALL isMeta=false. Key presence: parentUuid/uuid/timestamp/durationMs/messageCount/type/subtype/sessionId/version/cwd/gitBranch/userType/entrypoint/isSidechain 100%, slug 93.2%, pendingWorkflowCount 5.2%, pendingBackgroundAgentCount 2.9%, sessionKind 0.8%; 9 distinct key sets. durationMs median 145,510 ms (min 663, max 984,080,246). All 4,633 are in MAIN transcripts; zero in any subagent transcript. Denominators: main-lane type:\"user\" 67,885, main-lane type:\"assistant\" 133,993, main-lane \"stop_reason\":\"end_turn\" 8,913 (subagent lanes add 161,885 / 298,142 / 13,864). csift census on one project directory returned 287, equal to the raw count of 287 turn_duration lines in that directory. `csift show @<SESSION> --uuid <uuid>` rendered `harness.meta.turn-duration L577 [turn duration: 12m 45s - durationMs=764630 messageCount=487]`. Binary writer: `function B_t(e,n,r,o,d){return{type:\"system\",subtype:\"turn_duration\",durationMs:e,budgetTokens:n?.tokens,budgetLimit:n?.limit,budgetNudges:n?.nudges,messageCount:r,pendingBackgroundAgentCount:o,pendingWorkflowCount:d,timestamp:new Date().toISOString(),uuid:oT(),isMeta:!1}}`, described in the shipped schema as '@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the Done in Ns / Waiting for N agents line.'",
"rule": "One observation per physical line whose parsed subtype == 'turn_duration'; presence rate = lines carrying the key over all such lines; median over the 4,633 integer durationMs values. Denominator counts are per-line rg counts of the literal type marker, bucketed main vs subagent by path.",
"note": "Everything structural held: uuid + timestamp + parentUuid + isMeta:false on 100% of records, slug at 93%, sessionKind rare, integers only, and csift's gated leaf addresses it by both --line and --uuid. Only the median and the user-record denominator needed correcting."
}
]
},
{
"id": "QT-010",
"area": "queue-and-telemetry",
"behavior": "Two OPTIONAL integer fields ride a `turn_duration` record: `pendingWorkflowCount` (236 of 4,581 records = 5.2%, values 1-3, observed from Claude Code 2.1.156) and `pendingBackgroundAgentCount` (133 of 4,581 = 2.9%, values 1-7, observed from 2.1.159). Across 4,587 such records corpus-wide the only optional keys of any kind are `slug`, `sessionKind` and those two - no background-shell count field exists anywhere.",
"depends": "the fabricated `harness.meta.turn-duration` excerpt prints only the fields actually present, so a missing optional field must read as absent rather than as zero; the absence of a shell count is why csift must scan the whole main transcript for open background work instead of reading this record.",
"code": [
{
"path": "src/model/record.rs",
"lines": "177-180",
"snippet": " /// `turn_duration`: background agents still running at turn end (the REPL's\n /// \"Waiting for N agents\" line). Optional; measured on ~3% of records.\n #[serde(default, rename = \"pendingBackgroundAgentCount\")]\n pub pending_background_agent_count: Option<serde_json::Value>,"
},
{
"path": "src/search/record_text.rs",
"lines": "124-203",
"snippet": " }\n for (key, v) in [\n (\"messageCount\", rec.message_count.as_ref()),\n (\n \"pendingBackgroundAgentCount\",\n rec.pending_background_agent_count.as_ref(),\n ),\n (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),"
}
],
"instrument": "`rg -c '\"subtype\":\"turn_duration\"' <transcript>` for the denominator and `rg -c 'pendingBackgroundAgentCount' <transcript>` for the numerator; counting rule = turn_duration lines carrying the key over all turn_duration lines. For the closed optional set, run a key-set census over every matched line (one key-set observation per record) and report the distinct sorted key sets - expect zero keys containing `Shell`.",
"located": {
"claude_code": "2.1.159",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 5.1; src/model/record.rs:160-177 comment; CHANGELOG 0.10.0 (five promoted leaves); src/model/record.rs turn_duration fields"
},
"first_seen_claude_code": "2.1.156",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each project directory separately: rg -NI --no-heading '\"subtype\":\"turn_duration\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (distinct sorted key sets, value ranges, first version carrying each optional key); plus rg -cNI --no-heading 'budgetTokens' / 'budgetLimit' / 'budgetNudges' / 'briefHiddenCount' per directory; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,200}\"turn_duration\".{0,400}'",
"observed": "Over 4,633 turn_duration records: pendingWorkflowCount on 241 (5.2%), values {1,2,3}, earliest version 2.1.156; pendingBackgroundAgentCount on 136 (2.9%), values {1..13}, earliest version 2.1.159. Exactly 9 distinct key sets; the only keys that vary across them are slug, sessionKind, pendingWorkflowCount and pendingBackgroundAgentCount. No key anywhere in the 4,633 records contains the substring 'hell' (no shell count field). budgetTokens/budgetLimit/budgetNudges/briefHiddenCount appear 40/32/9/6 times corpus-wide but NONE of them as a top-level key of a turn_duration record - every occurrence is inside a type:\"user\" or type:\"assistant\" message payload (prose). The 2.1.258 writer nevertheless emits them: `...durationMs:e,budgetTokens:n?.tokens,budgetLimit:n?.limit,budgetNudges:n?.nudges,messageCount:r,...`, and the shipped schema documents them as 'Output tokens spent this turn toward the token budget', 'Turn token-budget ceiling', 'Budget-nudge count this turn'.",
"rule": "One key-set observation per turn_duration record; presence rate = records carrying the key over 4,633; value range = the distinct integers seen; 'earliest version' = the minimum `version` string on a record carrying the key. Corpus check for a budget field = a top-level key on a parsed turn_duration record, not a substring anywhere in the file.",
"note": "Two corrections matter. The observed pendingBackgroundAgentCount ceiling is 13, not 7 - a stated range invites a reader to treat it as a bound. And the 'only optional keys' inventory is a corpus fact that the shipped schema already exceeds by three budget fields; csift would render them as absent rather than reporting an unknown key."
}
]
},
{
"id": "QT-011",
"area": "queue-and-telemetry",
"behavior": "`pendingBackgroundAgentCount` is never emitted as the literal 0 - absence of the key means zero, not unknown (136 numeric occurrences corpus-wide, values 1 through 13, zero occurrences of the value 0). The field's relation to a transcript-replayed set of open async agents is looser than a match: over 3,316 turn_duration observations on four specimen transcripts the two agreed 2,292 times, and among the 59 records where the field is present 20 agreed exactly while 28 were off by exactly +1 on the replay side - so the field is best read as the harness's own count at that instant, not as something a replay can reconstruct.",
"depends": "any consumer reading the field must treat an absent key as 0; the residual under-count comes from agents that notified and were then resumed (a notification is not terminal) and from agents spawned in a child lane, so the field is a lower bound on open async work.",
"code": [
{
"path": "src/live/background.rs",
"lines": "63-74",
"snippet": "pub(crate) enum BgState {\n /// Launched, no completion carrier names it yet.\n Open,\n Completed,\n Failed,\n Killed,\n /// Claude Code's own orphan reconciliation at the next session start, or an\n /// explicit `stopped` status.\n Stopped,\n /// A Monitor whose timeout fired (the `[Monitor timed out …]` event).\n TimedOut,\n}"
}
],
"instrument": "`rg -c '\"pendingBackgroundAgentCount\":\\s*0' ~/.claude/projects -g '*.jsonl'` returns no matches while `rg -c '\"pendingBackgroundAgentCount\"'` returns many; counting rule = one observation per matching line. For the agreement figure, replay a transcript maintaining the set of launched-but-unreturned async agents and compare the set size against the field at each turn_duration record.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for d in ~/.claude/projects/*/; do rg -oNI '\"pendingBackgroundAgentCount\"\\s*:\\s*[0-9]+' -g '*.jsonl' \"$d\"; done | sort | uniq -c | sort -rn # plus, for the agreement half, a four-specimen replay: open an async agent on a tool_result line carrying \"status\":\"async_launched\" (keyed by its \"tool_use_id\"), close it at the first later line naming that id in <tool-use-id>...</tool-use-id>, and compare the open-set size against pendingBackgroundAgentCount at every \"subtype\":\"turn_duration\" line",
"observed": "zero-probe: 136 lines corpus-wide carry the field as an actual number; the value histogram is 1:59, 2:32, 3:17, 4:9, 5:5, 7:4, 9:2, 8:2, 6:2, 13:1, 12:1, 11:1, 10:1 - the literal 0 never appears. (398 lines contain the bare key STRING; the 262-line difference is escaped prose quoting the field name, not a field.) Replay: 3316 turn_duration observations, replayed open-agent set equal to the field on 2292; among the 59 records where the field is actually present, 20 exact, 28 off by exactly +1 on the replay side, 8 off the other way; 985 records with the field ABSENT had >=1 open agent under the replay rule.",
"rule": "zero-probe: one observation per physical line matching the unescaped numeric form, corpus-wide, scoped one project directory per rg invocation. Replay: one observation per turn_duration record; `open at that line` = an async_launched tool_result earlier in the file whose tool_use id has not yet appeared inside a <tool-use-id> tag; an absent field counts as 0.",
"note": "First half CONFIRMED with an exact instrument: the value 0 is never serialized, and the value range is 1-13 (wider than the 1-7 the sibling claim records). Second half NOT reproduced: the stated 608-of-628 agreement, and its claim that every mismatch is an under-count on the transcript side, both fail under the async-agent join csift itself documents (src/live/background.rs lines 8-9 and 19-21) - my replay errs the OTHER way, over-counting on 1,013 observations against 11 under-counts. The residual is dominated by launches whose completion carrier never names the tool_use id (only 5 of 208 launches were still open at end of file, yet a single such leak keeps the set +1 for the rest of the transcript - exactly the +1 offset seen on 28 of the 59 field-bearing records). The original counting rule for `launched-but-unreturned async agent` is not recoverable from the claim text, so the agreement figure should be dropped rather than restated. Code site verified verbatim: src/live/background.rs:63-74 is the BgState enum exactly as quoted."
}
]
},
{
"id": "QT-012",
"area": "queue-and-telemetry",
"behavior": "A `turn_duration` record IS written while BACKGROUND work is still open: of 3,314 such records on four specimen transcripts, 2,739 (82.6%) were emitted with at least one background shell open, and on 59 the harness's own `pendingBackgroundAgentCount` was >= 1 (190 more carried a non-zero `pendingWorkflowCount`). An end-of-turn telemetry record therefore says the turn ended, never that the session finished its work.",
"depends": "this is the exact hole the `idle-background-open` verdict fills: csift must join launches to completions itself rather than reading a turn-end record as a stop signal.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "11-14",
"snippet": " /// The turn ended (a clean end_turn) but N background task(s) the lens counts have\n /// not returned - neither running nor stopped: by design (a dev server, a watcher)\n /// or not, csift cannot tell. Never satisfies `--until stop`.\n IdleBackgroundOpen,"
}
],
"instrument": "Replay a transcript maintaining the set of `run_in_background` launches with no `<task-notification>` naming them yet; at each `subtype:\"turn_duration\"` line record that set size and the line key set. Counting rule = one observation per turn_duration record; `open at that instant` = launched earlier in the file with no completion carrier seen.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python replay over the four specimen transcripts: record every `\"subtype\":\"turn_duration\"` line; open a background shell at the first line matching `running in background with ID: ([A-Za-z0-9_-]+)` and close it at the first later line naming that id inside <task-id>...</task-id>; count open shells at each turn_duration line, and read pendingBackgroundAgentCount / pendingWorkflowCount off the record itself",
"observed": "3,314 turn_duration records; 1,872 background-shell launches; 2,739 of 3,314 records (82.6%) were emitted with at least one background shell open. The harness's own pending counters were non-zero on the same records: pendingBackgroundAgentCount present on 59 records (values 1-9) and pendingWorkflowCount present on 190. Per specimen the open-shell counts were 1639/1771, 604/1005, 427/430 and 69/108.",
"rule": "one observation per turn_duration record; `open at that instant` = launched earlier in the same file with no completion carrier naming its id seen yet. The pending-counter figures are one observation per turn_duration record, an absent key counted as zero.",
"note": "The law reproduces and is far stronger than stated - open background work at a turn-end record is the common case (83%), not the 16% the claim implies. The async-agent half is restated using the harness's own field rather than a replayed set, because the replay rule over-counts (see QT-011). Code site verified verbatim: src/live/verdict.rs:11-14 is the IdleBackgroundOpen doc comment and variant exactly as quoted."
}
]
},
{
"id": "QT-013",
"area": "queue-and-telemetry",
"behavior": "A `turn_duration` record emitted while a background SHELL is still running carries no field describing it - 2,739 of 2,739 measured records with an open shell had no shell-related key of any kind, and the key union over all 3,314 measured records is 18 keys whose only optional members are `slug`, `pendingWorkflowCount` and `pendingBackgroundAgentCount`. The pending counts cover background agents and workflows only, so the harness writes nothing on disk about a still-running shell at end of turn.",
"depends": "csift cannot read open-shell state from end-of-turn telemetry, so `status` joins the session registry, the transcript tail, child lanes, the task list and a process probe, and its background section - a whole-transcript scan for launches and completions - is the only instrument for a running shell.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "124-203",
"snippet": " }\n for (key, v) in [\n (\"messageCount\", rec.message_count.as_ref()),\n (\n \"pendingBackgroundAgentCount\",\n rec.pending_background_agent_count.as_ref(),\n ),\n (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),"
}
],
"instrument": "Launch a `run_in_background` shell, let the turn end, then read the newest end-of-turn record with `csift search '' <target> -t harness.meta.turn-duration --max-count -1 --no-truncate`: durationMs and messageCount only, no shell field. Counting rule = the key set enumerated per record over records known to have had an open shell.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; CHANGELOG 0.10.0 (measured on 100 turn_duration records emitted with an open shell); SPEC.md section 6 v0.10.0 ledger item 1; SPEC.md section 5.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "same four-specimen replay as QT-012, then a key-set census restricted to the turn_duration records that had >=1 open background shell: collect sorted(json.loads(line).keys()) per record and count distinct key sets, plus a scan of the key union for any key containing 'shell', 'bash' or 'command'",
"observed": "2,739 turn_duration records with an open shell fall into 5 distinct key sets. The union of ALL keys over all 3,314 records is exactly 18: cwd, durationMs, entrypoint, gitBranch, isMeta, isSidechain, messageCount, parentUuid, pendingBackgroundAgentCount, pendingWorkflowCount, sessionId, slug, subtype, timestamp, type, userType, uuid, version. No key contains 'shell', 'bash' or 'command'. The only optional keys are slug (absent on 20 records), pendingWorkflowCount (190) and pendingBackgroundAgentCount (59).",
"rule": "one key-set observation per turn_duration record known to have had an open shell at that line; the substring test for a shell-related key is run over the key union, not over values.",
"note": "Holds, at 27x the sample: the shell-invisibility is total. Two of the three code sites are verbatim at the cited lines (src/live/background.rs:4-7, src/cli/live_args.rs:43-48); the record_text.rs block sits one line lower than cited."
}
]
},
{
"id": "QT-014",
"area": "queue-and-telemetry",
"behavior": "A `turn_duration` record is NEVER written while a synchronous tool call is in flight: across 628 such records on four specimen transcripts the count of unreturned `tool_use` ids at that line was 0 in 628 of 628.",
"depends": "the record is therefore usable as proof that the synchronous lane was clear at that instant, but not as a turn-end detector, because its presence is not guaranteed at every clean end.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "19",
"snippet": "\"stop_hook_summary\" => Some(Class::MetaStopHooks),"
}
],
"instrument": "Replay one transcript line by line maintaining a running set of `tool_use` ids minus the seen `tool_result` `tool_use_id`s; at every `subtype:\"turn_duration\"` line record the set size. Counting rule = one observation per turn_duration record; the expected census is 0 open ids in every observation.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "two-pass python replay over the four specimen transcripts: pass 1 records, for every tool_use id (regex `\"id\":\"(toolu_[^\"]+)\"` on `\"type\":\"assistant\"` lines), its open line and the line of its `\"tool_use_id\"` result on a `\"type\":\"user\"` line; pass 2 counts, at every `\"subtype\":\"turn_duration\"` line L, the ids with open_line < L < result_line",
"observed": "3,314 turn_duration records; the in-flight count was 0 in 3,314 of 3,314 (per specimen 1771/1771, 1005/1005, 430/430, 108/108). 28,948 tool_use ids were opened across the four files and every single one has a matching tool_result (0 never-returned); only 2 pairs are written out of file order (the result line precedes the tool_use line), and a naive single-pass open-set that cannot see those 2 pairs is what makes a same-file replay report a phantom 1-2 open calls.",
"rule": "one observation per turn_duration record; a tool call is `in flight` at that line iff its tool_use record is earlier in the file and its tool_result record is strictly later. Ids never returned anywhere in the file are excluded (there were none).",
"note": "Reproduced exactly at 5.3x the sample, and the mechanism behind the only plausible counter-example is now pinned: 2 of 28,948 tool_use/tool_result pairs land out of file order, so an interval test (open < L < result) rather than a running open-set is the counting rule a stranger must use. Code site verified verbatim: src/model/classify_promoted.rs:16-21 is the `\"system\" => match self.subtype` arm exactly as quoted."
}
]
},
{
"id": "QT-015",
"area": "queue-and-telemetry",
"behavior": "A `turn_duration` record fires almost exclusively at a clean `end_turn` boundary: the preceding assistant `stop_reason` was `end_turn` in 3,267 of 3,314 records, `stop_sequence` in 45 and `tool_use` in 2. It is rare but NOT impossible after an interrupted or rejected turn end: 2 of 174 interrupt-marked gaps and 2 of 41 rejection-marked gaps carried one. Read it as a strong clean-boundary hint, never as a guarantee, and never as a segmentation input.",
"depends": "csift may read the record as a clean-boundary marker but never as a segmentation input; turn segmentation stays on the four turn-opening record shapes.",
"code": [
{
"path": "src/live/verdict.rs",
"lines": "309-314",
"snippet": " let eot_shape = main_tail.unreturned_use.is_none()\n && (main_tail\n .last_stop_reason\n .as_deref()\n .is_some_and(|s| s == \"end_turn\")\n || registry_shell);"
}
],
"instrument": "Segment a transcript into turn-end gaps (the records between the last assistant record and the next turn opener) and bucket each gap by the previous assistant `stop_reason` and by whether the gap contains a `turn_duration`. Counting rule = one gap per turn end; interrupt-shaped = a user record arriving while the previous assistant `stop_reason` is `tool_use`.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md section 6.13"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python replay over the four specimen transcripts: group assistant records by message id, take each message's last non-null `\"stop_reason\"` and its last record line; for every `\"subtype\":\"turn_duration\"` line take the stop_reason of the nearest preceding assistant message; separately, for every user record containing `[Request interrupted by user` or `The user doesn't want to proceed with this tool use`, test whether a turn_duration line falls between it and the first record of the next assistant message",
"observed": "3,314 turn_duration records: preceding assistant stop_reason was end_turn on 3,267 (98.6%), stop_sequence on 45, tool_use on 2. Interrupt markers: 174 total, 2 of them followed by a turn_duration before the next assistant record (both in one specimen). Tool/plan rejection markers: 41 total, 2 followed by a turn_duration (same specimen; 10 lines carry both markers).",
"rule": "one observation per turn_duration record for the stop_reason join; one observation per marker record for the gap tests, where the gap runs from the marker line to the first record of the next assistant message.",
"note": "The direction holds at 99%, but the two absolute zeros in the claim do not survive a 6x larger sample: interrupt-shaped and rejection-shaped gaps each produced 2 turn_duration records, and the preceding stop_reason vocabulary includes `stop_sequence` (45 records), which the claim does not mention. Code site verified verbatim: src/live/verdict.rs:309-314 is the `eot_shape` binding exactly as quoted."
}
]
},
{
"id": "QT-016",
"area": "queue-and-telemetry",
"behavior": "A `turn_duration` record is usually but not always written at a clean `end_turn`: it was present in 3,267 of 3,554 measured clean turn-end gaps and absent from 287. Absence is concentrated in a few Claude Code versions (2.1.217 wrote one in 130 gaps, 2.1.219 ten in 77, 2.1.233 fifty-seven in 130) while eleven other versions in the sample, including 2.1.258, are at or above 98.8%. Its absence is therefore still no evidence that a turn is running - but its presence is close to routine on current versions.",
"depends": "csift never reads the presence or absence of an end-of-turn telemetry record as proof that a session finished; `status` derives its verdict from the tail state machine, the registry and the background scan instead.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "178-184",
"snippet": "/// fields only, stable order - so the leaf is both matchable and legible:\n/// - turn-duration: `[turn duration: 1m 5s · durationMs=64911 messageCount=908\n/// pendingBackgroundAgentCount=2]`;\n/// - stop-hooks: `[stop hooks: count=N errors=M prevented=false]` + one `command (Nms)`\n/// line per hook (the commands are the record's only text);\n/// - snapshot: `[file-history snapshot at <ts>: <path>@vN, …]` (paths sorted) or\n/// `[file-history delta at <ts>: <path>@vN backup=<name>]` (the `backup=` suffix"
}
],
"instrument": "`csift search '' <target> -t harness.meta.turn-duration --count-by turn` against `csift search '' <target> -t user.message -c`; expected: strictly fewer turn_duration records than closed turns. Counting rule = one record per emitting turn, one gap per turn end.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md section 6.13; dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python replay over the four specimen transcripts: group assistant records by message id; for each message whose last stop_reason is `end_turn`, the gap runs from its last record line to the first record line of the next assistant message; test whether any `\"subtype\":\"turn_duration\"` line falls in that gap; bucket every gap by the `version` string of the assistant message that opened it",
"observed": "3,554 clean end_turn gaps, 3,267 carry a turn_duration record (91.9%), 287 do not. Per specimen: 1746/1757, 989/996, 425/621, 107/180. Per Claude Code version the presence rate is >= 0.988 at 11 of the 15 versions represented (2.1.150 24/24, 2.1.156 82/82, 2.1.159 621/623, 2.1.177 443/448, 2.1.186 35/35, 2.1.191 541/545, 2.1.218 11/11, 2.1.220 916/918, 2.1.227 31/31, 2.1.228 323/327, 2.1.251 153/154, 2.1.258 19/19) and collapses at three: 2.1.217 1/130, 2.1.219 10/77, 2.1.233 57/130.",
"rule": "one gap per assistant message whose last stop_reason is end_turn; `present` = at least one turn_duration line strictly inside the gap. Grouping assistant records by message id is load-bearing: treating each assistant RECORD as a turn end (a multi-block message emits several) roughly halves the measured rate and is what produces a ~48% figure.",
"note": "The law survives (287 clean ends carry no record, so absence proves nothing) but the ratio in the claim is wrong by a factor of two: 91.9% present, not 47.6%. The likely cause of the low original figure is segmenting on assistant RECORDS instead of assistant MESSAGES; the version breakdown also shows the gap is a per-version behaviour, not a steady coin flip. The second code site (src/live/verdict.rs:309-314) is verbatim at its cited lines."
}
]
},
{
"id": "QT-017",
"area": "queue-and-telemetry",
"behavior": "The rendered end-of-turn lines the operator sees never land on disk - the `turn_duration` record is their structured body, and the shipped 2.1.258 binary says so in one string: `REPL renders the 'Done in Ns' / 'Waiting for N agents' line. From internal SystemMessage 'turn_duration'.` Corpus-wide the phrase `Cogitated for` appears on 56 lines (27 assistant, 24 user, 4 attachment, 1 queue-operation) and the wording `waiting for N background agents to finish` on 10 lines (4 user, 3 last-prompt, 1 assistant, 2 attachment) - zero on any `system` record in both cases.",
"depends": "csift must source turn timing and pending-agent facts from `durationMs` and `pendingBackgroundAgentCount` only; searching the transcript for the rendered phrases finds quoted prose, never telemetry.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "197-204",
"snippet": " for (key, v) in [\n (\"messageCount\", rec.message_count.as_ref()),\n (\n \"pendingBackgroundAgentCount\",\n rec.pending_background_agent_count.as_ref(),\n ),\n (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),\n ] {"
}
],
"instrument": "`rg -cNI 'Cogitated for' ~/.claude/projects -g '*.jsonl'`, then re-parse each hit line and bucket it by top-level `type`; counting rule = one observation per matching LINE. Expect zero on `system`.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; src/model/record.rs:160-177 comment; CHANGELOG 0.10.0 (five promoted leaves); src/model/record.rs turn_duration fields"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg \"Waiting for .{0,30}agent\" AND for d in ~/.claude/projects/*/; do rg -NI --no-messages 'Cogitated for' -g '*.jsonl' \"$d\"; done (same loop for -i 'waiting for [0-9]+ background agents to finish'), each hit line then re-parsed and bucketed by its top-level `type`",
"observed": "Binary, one string: `@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the 'Done in Ns' / 'Waiting for N agents' line. From internal SystemMessage 'turn_duration'.` (`Cogitated` also appears as a past-tense spinner verb alongside the `Cogitating` verb list). On disk: `Cogitated for` on 56 lines - 27 assistant, 24 user, 4 attachment, 1 queue-operation, ZERO on any `system` record; `waiting for N background agents to finish` on 10 lines - 4 user, 3 last-prompt, 1 assistant, 2 attachment, ZERO on any `system` record.",
"rule": "one observation per matching physical line, each line re-parsed as JSON and counted under its top-level `type`; the scan is scoped one project directory per rg invocation.",
"note": "The load-bearing part - zero occurrences on a `system` record, so searching for the rendered phrase finds only quoted prose - reproduces exactly, and the binary string confirming the REPL/record relationship is present verbatim in 2.1.258. Only the line counts moved (56 and 10, up from 11 and 20), which is itself the point: the counts grow because sessions keep quoting the phrases, and the type bucketing is what separates prose from telemetry. The second code site (src/model/taxonomy.rs:122-127) is verbatim at its cited lines."
}
]
},
{
"id": "QT-018",
"area": "queue-and-telemetry",
"behavior": "Claude Code writes a `type:\"system\"` / `subtype:\"away_summary\"` record whose top-level `content` is ALWAYS a string (1,630 of 1,630): the recap shown when the operator returns to a session after a blur of five minutes or more. The text is model-generated by a side call rather than templated (the 2.1.258 binary carries `Away summary cannot use tools` and an `away_summary_generate` telemetry event) and the feature is config-gated (`awaySummaryEnabled`). Measured corpus: 1,630 records, 1,587 of 1,630 contents distinct, content length min/median/max 76/219/8,333 characters (p99 314; one outlier). The trailing suffix ` (disable recaps in /config)` is a first-few-recaps-per-session nudge, not an early-version artefact: 185 records carry it, spanning every version in the sample up to 2.1.258, and it is on 91 of the first 93 recaps of a session.",
"depends": "csift promotes it to the gated leaf `harness.meta.away-summary` and matches on the VERBATIM `content` (a raw byte substring, so no synthesized marker is needed); it is the most valuable promoted prose, and because it is model-written it must never be presented as operator text.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "190",
"snippet": " Class::UserQueued | Class::MetaAwaySummary => rec.content_str().map(str::to_string),"
}
],
"instrument": "`rg -cNI '\"subtype\":\"away_summary\"' ~/.claude/projects -g '*.jsonl'` for the population (counting rule = one per matching line), then a content census over those lines for distinctness and length. `csift search '' <target> -t harness.meta.away-summary --no-truncate` must print the verbatim recap.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 5.1; src/model/taxonomy.rs:128-132 comment; CHANGELOG 0.10.0; SKILL.md label table (harness.meta.away-summary)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for d in ~/.claude/projects/*/; do rg -NI --no-messages '\"subtype\":\"away_summary\"' -g '*.jsonl' \"$d\"; done > away.jsonl then a python census over away.jsonl for content type, length, distinctness, the trailing-suffix rate and the key sets; render check: csift search '' @<session-prefix> -t harness.meta.away-summary --no-truncate --max-count 2",
"observed": "1,630 records (matching csift's own `search '' -t harness.meta.away-summary --count-by session` total of 1,630 across 39 sessions). `content` is a string on 1,630 of 1,630. Distinct contents 1,587 of 1,630. Length min/median/max 76 / 219 / 8,333 characters; p90 271, p99 314, exactly one record over 1,760. The trailing suffix ` (disable recaps in /config)` is on 185 records spanning 2026-05-25 to 2026-09-02 and every CC version in the sample including 2.1.258; by in-session rank it is on 39/39 first recaps, 30/31 second, 22/23 third, then decays (85 of 1,493 later ones). Three key sets: the base 15, +slug (1,533), +sessionKind (10). The csift render prints the verbatim recap under `⚙ harness.meta.away-summary` with its line number.",
"rule": "one observation per physical line matching the unescaped `\"subtype\":\"away_summary\"` form, scoped one project directory per rg invocation; distinctness is over exact content strings; the suffix test is an exact endswith; in-session rank orders a session's records by timestamp.",
"note": "Everything structural holds - always a string, essentially all distinct, short prose, config-gated, model-written. Two corrections matter. The max length is 8,333 rather than 1,760 (a single outlier; p99 is 314), so a consumer sizing a buffer on the old number would clip. And `the earliest ones carry the trailing suffix` is wrong: the suffix is a per-SESSION nudge on the first few recaps and is still emitted at 2.1.258, so its presence dates nothing. Code site: the classify arm (src/model/classify_promoted.rs:18) is verbatim; the render arm sits one line lower than cited."
}
]
},
{
"id": "QT-019",
"area": "queue-and-telemetry",
"behavior": "The away recap is capped (`[awaySummary] recap capped from `) and is SKIPPED in SEVEN named conditions in 2.1.258: cache age unknown, cache stale, at or near a rate limit, draft input present, background work pending, a loop wakeup pending, and a StructuredOutput recap already present. Four further paths abort generation (a new turn already running, no saved cache-safe params, a failed params rebuild, a failed generation), and the whole feature is config-gated behind `awaySummaryEnabled`. Its absence after a long gap is therefore expected behaviour, not a missing record: on two specimen transcripts only 59% and 34% of gaps of five minutes or more were followed by a recap.",
"depends": "csift must not treat a missing away recap as evidence about the gap; the leaf is searchable when present and silent when the harness skipped it.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "18",
"snippet": " \"away_summary\" => Some(Class::MetaAwaySummary),"
}
],
"instrument": "Run `strings -n 6` over the shipped Claude Code binary for the located version and grep for `awaySummary` to read the skip-condition list and generation telemetry keys; counting rule = one observation per extracted string. Cross-check on disk by finding session gaps of five minutes or more that carry no `away_summary` record.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > s.txt; rg -n 'awaySummary' s.txt plus an on-disk cross-check: for two specimen transcripts, count gaps of >= 300 s between consecutive timestamped records and how many are followed by an away_summary record before the next such gap",
"observed": "SEVEN skip strings in 2.1.258, contiguous in the binary: `[awaySummary] skipped: cache age unknown\"`, `[awaySummary] skipped: cache stale`, `[awaySummary] skipped: at or near rate limit*`, `[awaySummary] skipped: draft input present`, `[awaySummary] skipped: background work pending`, `[awaySummary] skipped: loop wakeup pending`, `[awaySummary] skipped: StructuredOutput recap present`. Cap confirmed by `[awaySummary] recap capped from `. Four further abort paths: `[awaySummary] ccr recap dropped: new turn already running`, `[awaySummary] no CacheSafeParams saved, skipping `, `[awaySummary] fallback params rebuild failed: `, `[awaySummary] generation failed: `. Adjacent: `awaySummaryEnabled`, `away_summary_generate`, `Away summary cannot use tools`, ` (disable recaps in /config)`, `pendingAgents`, `pendingWorkflows`. On disk: 150 gaps >= 300 s in one specimen with 89 followed by a recap (59%), and 383 gaps with 130 followed by a recap (34%).",
"rule": "one observation per extracted binary string; on disk, one observation per gap of >= 300 s between consecutive timestamped records, `followed by a recap` meaning an away_summary record appears before the next such gap.",
"note": "Refined upward: the skip list is seven conditions, not five - the two the claim omits are cache-related (`cache age unknown`, `cache stale`), which matters because they are the ones that fire on a fresh or long-running session rather than on operator state. The on-disk cross-check the claim asks for now has numbers. Code site verified verbatim: src/model/classify_promoted.rs:18."
}
]
},
{
"id": "QT-020",
"area": "queue-and-telemetry",
"behavior": "An away recap never re-enters the conversation: 1,629 of 1,630 measured `away_summary` contents never re-appear anywhere downstream in the surviving conversation of the same file. The single exception re-appeared on a later `type:\"user\"` record, i.e. it was quoted back in, not delivered by the harness.",
"depends": "csift classifies the leaf LLM-invisible on that evidence and words it as `not in the surviving conversation`; treating a model-generated recap as delivered context would inflate every accounting of what the assistant actually received.",
"code": [
{
"path": "src/cli/search_args.rs",
"lines": "558-560",
"snippet": " pub fn reaches_gated(&self, c: Class) -> bool {\n !self.labels.is_empty() && self.label_filter().selected(c.path())\n }"
},
{
"path": "src/model/taxonomy.rs",
"lines": "128-132",
"snippet": " /// `harness.meta.away-summary` - the `system`/`away_summary` recap the harness\n /// generates (a side model call, config-gated) when the operator returns after 5+\n /// minutes away. Model-generated prose, yet no `message{}`: shown in the UI, not in\n /// the surviving conversation (measured 1,196/1,198 never re-appear downstream).\n MetaAwaySummary,"
}
],
"instrument": "`csift search '' -t harness.meta.away-summary --count-by session` for the population; then for each record take a distinctive phrase of its text and run `csift search '<phrase>' <same target>` looking for a later non-away record. Counting rule = away_summary records whose text recurs on any later record of the same file.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "SPEC.md section 5.1; src/model/taxonomy.rs:128-132 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each of the 41 transcripts containing an away_summary record, load the file, and for every away_summary take its content minus the ` (disable recaps in /config)` suffix, take the first 60 characters, JSON-escape them, and scan every LATER line that is not itself an away_summary for that escaped string; then re-parse the one hit and read its top-level `type`",
"observed": "1,630 away_summary records checked across 41 files; 1 recurs downstream, 1,629 never do. The single recurrence lands on a later `type:\"user\"` record carrying a `message{}` object (i.e. it re-entered as operator/injected text, not as harness output).",
"rule": "one observation per away_summary record; `recurs` = a 60-character JSON-escaped prefix of its content appears on any later line of the same file that is not itself an away_summary record.",
"note": "The law holds and is stronger than stated (99.94% never recur). Two corrections: the numbers, and the fact that the one exception is a user record rather than harness output - which is the right way to word it, since a recap quoted back by a human is not the harness delivering it. The taxonomy.rs doc comment is verbatim at its cited lines but embeds the stale 1,196/1,198 count and should be updated in the same pass. The search_args.rs gate moved six lines down."
}
]
},
{
"id": "QT-021",
"area": "queue-and-telemetry",
"behavior": "Claude Code writes a type:\"system\" / subtype:\"stop_hook_summary\" record per Stop-hook run: an execution ledger carrying hookCount (observed value set {1,3,6,7,8,9}), hookInfos[] of {command, durationMs} elements where durationMs is OPTIONAL (41,382 of 41,412 measured elements carry it, 30 carry command alone), hookErrors[] (non-empty on 30 of 5,644 measured records), preventedContinuation, stopReason, hasOutput and toolUseID; all 5,644 measured records also carry level:\"suggestion\", which is one of the four values (info/notice/suggestion/warning) the 2.1.258 schema allows.",
"depends": "csift promotes it to the gated leaf `harness.meta.stop-hooks` and renders a fabricated `[stop hooks: count=N errors=M prevented=B]` head (M = the LENGTH of `hookErrors`, not its text) plus one line per `hookInfos` element: `command (Nms)` when `durationMs` is present, the bare `command` when it is not, and no line at all for an element without a `command` string. The leaf is kept distinct from `harness.meta.hook`, which is the text a hook INJECTED, so run telemetry is never attributed to injected context.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "14-19",
"snippet": " match self.r#type.as_deref()? {\n \"queue-operation\" => self.queued_class(),\n \"system\" => match self.subtype.as_deref()? {\n \"turn_duration\" => Some(Class::MetaTurnDuration),\n \"away_summary\" => Some(Class::MetaAwaySummary),\n \"stop_hook_summary\" => Some(Class::MetaStopHooks),"
},
{
"path": "src/search/record_text.rs",
"lines": "218-232",
"snippet": " let prevented = rec.prevented_continuation.unwrap_or(false);\n let mut lines = vec![format!(\n \"[stop hooks: count={count} errors={errors} prevented={prevented}]\"\n )];\n if let Some(infos) = rec\n .hook_infos\n .as_ref()\n .and_then(serde_json::Value::as_array)\n {\n for info in infos {\n let Some(cmd) = info.get(\"command\").and_then(serde_json::Value::as_str) else {\n continue;\n };\n match Record::u64_field(info.get(\"durationMs\")) {\n Some(ms) => lines.push(format!(\"{cmd} ({ms}ms)\")),"
},
{
"path": "src/search/scan.rs",
"lines": "394-400",
"snippet": " // v0.10.0 promoted lines, each behind its own explicit-selector gate.\n || (gates.queued && QUEUED_FINDER.find(line).is_some())\n || (gates.turn_duration && TURN_DURATION_FINDER.find(line).is_some())\n || (gates.away_summary && AWAY_SUMMARY_FINDER.find(line).is_some())\n || (gates.stop_hooks && STOP_HOOKS_FINDER.find(line).is_some())\n || (gates.snapshot && SNAPSHOT_FINDER.find(line).is_some())\n || (gates.system && SUBTYPE_FINDER.find(line).is_some())"
}
],
"instrument": "`rg -NI '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl'` into a JSON reader for a key-set and value census; counting rule = one observation per matching line, and one per ARRAY ELEMENT for the `hookInfos` total. `csift search '' <target> -t harness.meta.stop-hooks --max-count 2` must print the fabricated head plus the command lines.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md section 5.1; SPEC.md v0.10.0 ledger; src/model/taxonomy.rs:133-138 comment; CHANGELOG 0.10.0; src/model/record.rs stop_hook_summary fields"
},
"first_seen_claude_code": "2.1.150",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) rg -NI --no-heading -F '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'json.loads each line; Counter over keys, hookCount values, hookInfos element key-sets, level, stopReason/preventedContinuation cross-tab, hookAdditionalContext type+len' (2) strings -n 200 ~/.local/share/claude/versions/2.1.258 | rg -o 'subtype:x\\(\"stop_hook_summary\"\\).{0,700}' (3) csift search '' <transcript> --no-subagents -t harness.meta.stop-hooks --max-count 2",
"observed": "5,644 stop_hook_summary lines, 0 unparseable. hookCount value set is exactly {1:2, 3:306, 6:2042, 7:702, 8:2, 9:2590} - the same six values the claim lists. hookInfos holds 41,412 elements: 41,382 with key-set (command, durationMs) and 30 with (command) only. hookErrors is a non-empty array on 30 records. level=='suggestion' on 5,644 of 5,644. hookCount / hookInfos / hookErrors / preventedContinuation / stopReason / hasOutput / toolUseID are present on 5,644 of 5,644 (hasOutput true on 5,641, false on 3). Earliest version carrying the record is 2.1.150 (24 records, earliest timestamp 2026-05-25T09:04:06Z), not 2.1.170; 2.1.258 carries 24. Binary schema at 2.1.258: hook_count:int, hook_infos:[{command, prompt_text?, duration_ms?}], hook_errors:[string], hook_additional_context:[string]?, prevented_continuation:bool, stop_reason?:string, has_output:bool, level:enum[\"info\",\"notice\",\"suggestion\",\"warning\"], tool_use_id?, hook_label?, total_duration_ms?, uuid, session_id; describe = \"@internal Summary of Stop/SubagentStop hook execution at turn end — which hooks ran, their output, and whether any prevented continuation. From internal SystemMessage 'stop_hook_summary'.\" csift printed '[stop hooks: count=6 errors=0 prevented=false]' followed by one '<command> (<N>ms)' line per hook entry; in the sampled transcript --count-by label gave harness.meta.stop-hooks 990 and harness.meta.hook 3, two separate keys.",
"rule": "One observation per matching jsonl line for record-level counts; one observation per ARRAY ELEMENT for the hookInfos total. A field counts as present when the key exists (a null value still counts as present). Corpus = every *.jsonl under ~/.claude/projects (7,781 files, 1,291,324 lines by the csift stats census).",
"note": "Behavior holds; only corpus-growth numbers and the hookInfos element shape needed correction. Two forward notes from the 2.1.258 binary schema, neither yet observed on disk here: the record gained optional hook_label (a sibling render path keys on hookLabel===\"PreToolUse\", so the same subtype can now describe hook events other than Stop/SubagentStop) and optional total_duration_ms and per-hook prompt_text. hookLabel appears on 0 of 5,644 corpus records, and a grep of src/ finds no csift field for hook_label, total_duration_ms or prompt_text - so a PreToolUse-labeled record would today render as if it were a turn-end Stop summary."
}
]
},
{
"id": "QT-022",
"area": "queue-and-telemetry",
"behavior": "stopReason is empty on 5,641 of 5,644 measured stop_hook_summary records; the 3 carrying the string 'Stop hook prevented continuation' are exactly the 3 that also carry preventedContinuation:true, so a blocked turn end is a rare, positively marked event.",
"depends": "the fabricated stop-hooks excerpt prints `prevented=<bool>` from `preventedContinuation`, which is the field a consumer should key on; the free-text `stopReason` is empty on almost every record and carries no independent information.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "211-221",
"snippet": " Class::MetaStopHooks => {\n let count = Record::u64_field(rec.hook_count.as_ref()).unwrap_or(0);\n let errors = rec\n .hook_errors\n .as_ref()\n .and_then(serde_json::Value::as_array)\n .map_or(0, Vec::len);\n let prevented = rec.prevented_continuation.unwrap_or(false);\n let mut lines = vec![format!(\n \"[stop hooks: count={count} errors={errors} prevented={prevented}]\"\n )];"
},
{
"path": "src/model/record.rs",
"lines": "195-201",
"snippet": " /// `stop_hook_summary`: the hooks that errored (an array; empty on most records).\n #[serde(default, rename = \"hookErrors\")]\n pub hook_errors: Option<serde_json::Value>,\n\n /// `stop_hook_summary`: true when a Stop hook blocked the turn from ending.\n #[serde(default, rename = \"preventedContinuation\")]\n pub prevented_continuation: Option<bool>,"
}
],
"instrument": "Parse every `stop_hook_summary` line and cross-tabulate `preventedContinuation` against a non-empty `stopReason`; counting rule = one observation per matching line.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -NI --no-heading -F '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'parse each line; cross-tabulate a non-empty stopReason against preventedContinuation===true and collect the distinct non-empty stopReason values'",
"observed": "5,644 records. stopReason non-empty on 3; preventedContinuation true on 3; the intersection is 3 (so the two sets are identical). The single distinct non-empty value is the string 'Stop hook prevented continuation', seen 3 times. stopReason is empty on the other 5,641.",
"rule": "One observation per matching jsonl line. 'Non-empty stopReason' = the value is a string whose strip() is not empty. Same corpus as QT-021.",
"note": "The interesting number - 3 blocked turn ends, and stopReason carrying no information the boolean does not already carry - is unchanged from the ledger; only the denominator grew with the corpus. The 2.1.258 schema confirms the shape: stop_reason is declared optional while prevented_continuation is a required boolean, so keying on the boolean is the right advice."
}
]
},
{
"id": "QT-023",
"area": "queue-and-telemetry",
"behavior": "The hookAdditionalContext field of a stop_hook_summary record, when present, was an EMPTY list in every one of the 4,916 measured instances - so the hook COMMANDS in hookInfos[] are the only text these records carry. Emptiness is a property of the measured corpus, not of the format: Claude Code 2.1.258 documents the field as the feed for hookSpecificOutput.additionalContext, so a Stop hook that emitted additionalContext would fill it.",
"depends": "csift joins the `hookInfos[].command` lines as the record text; treating `hookAdditionalContext` as the context the hooks contributed would yield nothing at all, and the fabricated head is what makes the leaf matchable.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "222-236",
"snippet": " if let Some(infos) = rec\n .hook_infos\n .as_ref()\n .and_then(serde_json::Value::as_array)\n {\n for info in infos {\n let Some(cmd) = info.get(\"command\").and_then(serde_json::Value::as_str) else {\n continue;\n };\n match Record::u64_field(info.get(\"durationMs\")) {\n Some(ms) => lines.push(format!(\"{cmd} ({ms}ms)\")),\n None => lines.push(cmd.to_string()),\n }\n }\n }"
},
{
"path": "src/model/taxonomy.rs",
"lines": "133-138",
"snippet": " /// `harness.meta.stop-hooks` - the `system`/`stop_hook_summary` execution ledger\n /// of the Stop hooks that ran at turn end (`hookInfos[].command` + durations,\n /// `hookErrors`, `preventedContinuation`). Distinct from [`Class::MetaHook`], which\n /// is the text a hook INJECTED into the model; this record is the run itself and\n /// its `hookAdditionalContext` is empty on every measured instance.\n MetaStopHooks,"
},
{
"path": "src/search/matcher.rs",
"lines": "506-513",
"snippet": " if args.reaches_gated(Class::MetaTurnDuration) {\n verifiable.push(b\"turn_duration\");\n }\n if args.reaches_gated(Class::MetaStopHooks) {\n verifiable.push(b\"stop_hook_summary\");\n }\n if args.reaches_gated(Class::MetaSnapshot) {\n verifiable.push(b\"file-history-\");"
}
],
"instrument": "Parse all `stop_hook_summary` lines and count the instances where `hookAdditionalContext` is present and its length is 0; counting rule = one observation per present instance (measured 4,865 of 4,865 empty).",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md section 5.1; SPEC.md v0.10.0 ledger; src/model/taxonomy.rs:133-138 comment"
},
"first_seen_claude_code": "2.1.170",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) rg -NI --no-heading -F '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'count records where hookAdditionalContext is present, its Python type, and len(value)==0; also bucket presence by the record version field' (2) strings -n 200 ~/.local/share/claude/versions/2.1.258 | rg -o 'hook_additional_context.{0,260}' (3) rg -n 'hookAdditionalContext|hook_additional_context' src/ --type rust",
"observed": "hookAdditionalContext present on 4,916 of 5,644 records; its value is a list on 4,916 of 4,916 and its length is 0 on 4,916 of 4,916 - no non-empty instance anywhere in the corpus. Presence is version-gated: 0 of the 728 records at 2.1.150 / 2.1.156 / 2.1.159 carry the key, and 100% of records carry it from 2.1.170 onward (2.1.258: 24 of 24), so the claim's first_seen_cc 2.1.170 is right for this FIELD. Binary describe: \"Non-error feedback from hookSpecificOutput.additionalContext — kept separate from hook_errors so the sanctioned feedback channel is not labeled an error. Absent in sessions persisted before this field existed.\" The src/ grep finds hook_additional_context only as the ATTACHMENT payload helper (Record::hook_additional_context_text, src/model/predicates.rs:336) that feeds harness.meta.hook - Record has no field for the stop_hook_summary key of the same name.",
"rule": "One observation per record where the key exists (measured 4,916 of 4,916 empty). Version buckets read the record's own version field. Same corpus as QT-021.",
"note": "Holds as a measurement, refined as a law. Actionable gap this verification exposes: csift's stop-hooks render reads hookCount, hookErrors, preventedContinuation and hookInfos only, and Record models no hookAdditionalContext field for this record type - so on the first transcript where a Stop hook returns additionalContext, that text would be silently absent from the excerpt while the taxonomy comment at src/model/taxonomy.rs:137 still asserts it 'is empty on every measured instance'. The comment should say 'on every instance measured through CC 2.1.258' or the field should be read."
}
]
},
{
"id": "QT-024",
"area": "queue-and-telemetry",
"behavior": "At Claude Code 2.1.258 a stop_hook_summary record and a turn_duration record are written as a pair at each turn end, stop_hook_summary first, with the turn_duration record naming it as parentUuid: 24 of each over 4 transcripts, strictly alternating, with no unpaired instance. Neither subtype is a safe turn counter on its own, because turn_duration is suppressed by the showTurnDuration user setting and stop_hook_summary is absent when no Stop hook is installed.",
"depends": "csift classifies the record as the gated searchable leaf `harness.meta.stop-hooks` and never as a turn boundary: `opens_turn` fires only on the four user-record cases, so a `type:\"system\"` stop-hook line is a turn MEMBER, never a delimiter. Turn counts stay keyed on the human's own records, which is what keeps them stable when either subtype is absent by configuration.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "14-19",
"snippet": " match self.r#type.as_deref()? {\n \"queue-operation\" => self.queued_class(),\n \"system\" => match self.subtype.as_deref()? {\n \"turn_duration\" => Some(Class::MetaTurnDuration),\n \"away_summary\" => Some(Class::MetaAwaySummary),\n \"stop_hook_summary\" => Some(Class::MetaStopHooks),"
},
{
"path": "src/model/exchange.rs",
"lines": "161-219",
"snippet": " #[must_use]\n pub fn opens_turn(&self) -> bool {\n self.is_genuine_user()\n || self.is_auq_answer_boundary()\n || self.is_plan_rejection_boundary()\n || self.is_peer_message_record()\n }"
},
{
"path": "src/search/scan.rs",
"lines": "394-400",
"snippet": " // v0.10.0 promoted lines, each behind its own explicit-selector gate.\n || (gates.queued && QUEUED_FINDER.find(line).is_some())\n || (gates.turn_duration && TURN_DURATION_FINDER.find(line).is_some())\n || (gates.away_summary && AWAY_SUMMARY_FINDER.find(line).is_some())\n || (gates.stop_hooks && STOP_HOOKS_FINDER.find(line).is_some())\n || (gates.snapshot && SNAPSHOT_FINDER.find(line).is_some())\n || (gates.system && SUBTYPE_FINDER.find(line).is_some())"
},
{
"path": "src/cli/show_stats_args.rs",
"lines": "84-88",
"snippet": " /// `42..` = turn 42 → the end). Fetches every user and assistant record of the named\n /// turns (reads a turn's whole back-and-forth); the gated non-record lines a turn\n /// carries (turn-duration, stop-hooks, away-summary, queue, snapshot, system) belong\n /// to no turn fetch: address them by `--line`/`--uuid`, or `search -t harness.meta`.\n /// The turn numbering matches the `·t<N>` in `search`'s exchange headers exactly."
}
],
"instrument": "`rg -c '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl'` and the same for `turn_duration`, compared PER FILE; counting rule = one record per matching line.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "(1) rg -NI --no-heading -H '\"subtype\":\"stop_hook_summary\"|\"subtype\":\"turn_duration\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'per-file and per-version counts of each subtype, plus the per-file S/T marker string in file order' (2) python3 -c 'in the largest transcript carrying both, collect every turn_duration uuid and count records naming one as parentUuid' (3) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{200}showTurnDuration.{200}'",
"observed": "At CC 2.1.258 the two are 1:1: 24 stop_hook_summary and 24 turn_duration records, spread over 4 transcripts (19/19, 2/2, 2/2, 1/1), and in every one of those 4 the marker string in file order is a strict alternation STSTST... - no transcript has a stop_hook_summary that is not immediately followed by a turn_duration. Across the whole corpus (54 files carrying either) the totals are 5,644 vs 4,632, but per-file the direction is mixed: 14 files have more stop_hook_summary, 8 files have FEWER, 32 are equal, and 0 files carry one type without the other. The largest transcript measured (158,923 lines) has 990 stop_hook_summary against 1,005 turn_duration - the opposite of the claim. In that same transcript 989 of the 990 stop_hook_summary uuids are the parentUuid of a turn_duration record, so the two are a linked pair at one turn end rather than independently clocked events. The corpus-wide surplus is concentrated in two version windows (2.1.231: 556 vs 112; 2.1.217: 139 vs 10; 2.1.208: 104 vs 4; 2.1.196: 41 vs 7) which show up as long unbroken S-runs (128 records at 2.1.220, 446 at 2.1.231); the 2.1.258 binary carries a per-user config toggle showTurnDuration (default true) described as 'Show \"Cooked for Nm Ns\" after each assistant turn', so those windows are turn_duration being suppressed by configuration, not stop_hook_summary firing extra times.",
"rule": "One record per matching jsonl line. Per-file comparison counts the two subtypes within one file and compares them; the S/T marker string preserves file order. The parent-chain count is one hit per record whose parentUuid equals a turn_duration uuid in the same file.",
"note": "New behavior: the claim's two observable predictions both fail at 2.1.258 - stop_hook_summary does NOT appear more often than turn_duration in the same file (24 vs 24 corpus-wide at that version; 990 vs 1,005 in the largest transcript measured), and it does NOT land in turn-end gaps carrying no turn_duration (every 2.1.258 transcript alternates S,T with no S-run of length 2 or more). The historical imbalance the claim rests on is explained by the showTurnDuration setting, so it was never a per-invocation-versus-per-turn law. csift's downstream posture is unaffected and still correct: it treats the record as searchable only and never as a turn boundary, which is right for a different reason - either subtype can be absent by configuration."
},
{
"claude_code": "2.1.258",
"csift": "0.10.2",
"date": "2026-09-03",
"verdict": "refined",
"instrument": "csift show --help text read against search/scan.rs (the `reach` closure admits a gated line only under a reaching selector or a --line/--uuid address; a --turn range is no address)",
"observed": "0.10.1 help promised EVERY record of the named turns while a --turn fetch admits no gated promoted line; 0.10.2 help names the exclusion and the --line/--uuid and search -t routes",
"rule": "help text is surface; the data path is the documented address exception",
"note": "no data-path change; the omission is now disclosed in the flag's help"
}
]
},
{
"id": "QT-025",
"area": "queue-and-telemetry",
"behavior": "file-history-delta is a distinct line type from file-history-snapshot: it is written per tracked file per write, carries a top-level timestamp (which the snapshot form does not - the snapshot's instant is nested at snapshot.timestamp) and holds messageId, snapshotMessageId, trackingPath and a backup object. Only version and backupTime are non-null on every delta; backupFileName is present but NULL on 1,454 of 1,985 and realParentDir is absent on 934 of 1,985.",
"depends": "csift renders both forms under the gated leaf `harness.meta.snapshot` with the tracked paths and versions as matchable text, and the byte prefilter admits them on the shared `\"file-history-` type prefix; the finer-grained delta form is not yet consumed by `recover`.",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "272-284",
"snippet": " if rec.is_type(\"file-history-delta\") {\n let path = rec.tracking_path.as_deref()?;\n let backup = rec.backup.as_ref();\n let version = ver(backup.and_then(|b| b.get(\"version\")));\n let at = rec.timestamp.as_deref().unwrap_or(\"?\");\n let name = backup\n .and_then(|b| b.get(\"backupFileName\"))\n .and_then(serde_json::Value::as_str)\n .map(|n| format!(\" backup={n}\"))\n .unwrap_or_default();\n return Some(format!(\n \"[file-history delta at {at}: {path}@v{version}{name}]\"\n ));"
},
{
"path": "src/recover/events.rs",
"lines": "197-202",
"snippet": " if let Some(snap) = rec.snapshot.as_ref() {\n if let Some(tfb) = snap.get(\"trackedFileBackups\").and_then(|v| v.as_object()) {\n for (path, entry) in tfb {\n if path_matches(target_file, path) {\n events.push(FileEvent {\n line_no,"
}
],
"instrument": "`csift search 'file-history-' <target> -t harness.meta.snapshot --max-count 4`; expected: both snapshot and delta excerpts. Counting rule = one record per write for the delta form, one per prompt for the snapshot form.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md v0.10.0 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) rg -NI --no-heading -H '\"type\":\"file-history-snapshot\"|\"type\":\"file-history-delta\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'per-type key-set counters, backup sub-key non-null vs null vs absent counts, deltas per messageId, distinct trackingPath' (2) csift search '' <transcript> -t harness.meta.snapshot --max-count 40 (3) csift show <transcript> --line <a delta line with a non-null backupFileName> (4) rg -n 'file-history-delta|trackingPath' src/recover/",
"observed": "4,062 snapshot lines and 1,985 delta lines corpus-wide. The two shapes are disjoint: snapshot key-set is {type, messageId, snapshot, isSnapshotUpdate} with a top-level timestamp on 0 of 4,062; delta key-set is {type, messageId, snapshotMessageId, trackingPath, backup, timestamp} with a top-level timestamp on 1,985 of 1,985. The snapshot's own instant is nested at snapshot.timestamp (4,062 of 4,062) alongside snapshot.trackedFileBackups (an object, empty on 158 of 4,062). Inside the delta backup object: version non-null 1,985/1,985, backupTime non-null 1,985/1,985, backupFileName present on 1,985 but NULL on 1,454 (non-null on 531), realParentDir absent on 934 (present and non-null on 1,051). Delta counting: 1,985 deltas over 1,553 distinct messageIds (1,121 messages with 1 delta, 432 with 2) and 1,337 distinct trackingPath values; newest delta timestamp 2026-09-02, so the form is live at 2.1.258. csift renders both under harness.meta.snapshot in one query - '[file-history snapshot at <ts>: ]' and '[file-history delta at <ts>: <path>@v1]' rows in the same result - and a delta whose backupFileName is a string renders the documented '... @v1 backup=<name>' suffix. rg over src/recover/ finds no reference to file-history-delta or trackingPath.",
"rule": "One observation per matching jsonl line for line and key counts; for backup sub-keys, three disjoint buckets per record (non-null / present-but-null / absent). Deltas per messageId is one observation per delta record grouped by its messageId.",
"note": "Consequence of the backupFileName nulls, worth a doc line: the delta excerpt documented at src/search/record_text.rs:184 as '[file-history delta at <ts>: <path>@vN backup=<name>]' omits the backup= suffix on 73% of real deltas, because snapshot_record_text (src/search/record_text.rs:276-280) correctly drops a null through and_then(as_str). The rendered form is right; the doc comment reads as if the suffix were unconditional. recover still does not consume the delta form, as the claim says."
}
]
},
{
"id": "QT-026",
"area": "queue-and-telemetry",
"behavior": "Exactly five jsonl line types carry the queue and telemetry facts while carrying no `message{}` object: `queue-operation` (with the queued content), the `system` subtypes `turn_duration`, `away_summary` and `stop_hook_summary`, and the `file-history-snapshot` / `file-history-delta` pair.",
"depends": "csift promotes each to exactly one LLM-invisible leaf (`user.queued`, `harness.meta.turn-duration`, `harness.meta.away-summary`, `harness.meta.stop-hooks`, `harness.meta.snapshot`) and admits them to a scan ONLY behind an explicit `-t` selector reaching that leaf or a `show` address, so a bare scan, a bare role selector, a `-T`-only filter and a flagless `--count-by label` never parse them.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "14-28",
"snippet": " match self.r#type.as_deref()? {\n \"queue-operation\" => self.queued_class(),\n \"system\" => match self.subtype.as_deref()? {\n \"turn_duration\" => Some(Class::MetaTurnDuration),\n \"away_summary\" => Some(Class::MetaAwaySummary),\n \"stop_hook_summary\" => Some(Class::MetaStopHooks),\n // The compaction boundary has its own leaf (`classify`, D7); every OTHER\n // system subtype is the harness talking to its own UI (v0.10.1 catch-all:\n // informational, api_error, model_refusal_*, agents_killed, local_command,\n // scheduled_task_fire, and whatever a later build adds).\n \"compact_boundary\" => None,\n _ => Some(Class::MetaSystem),\n },\n \"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),\n _ => None,"
}
],
"instrument": "`jq -r '[.type, (.subtype // \"\")] | @tsv' <a transcript> | sort | uniq -c` for the census (counting rule = one line per type/subtype pair), then `csift search '' <target> -t harness.meta --count-by label` against the same command without `-t`: the promoted leaves appear only in the gated run.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "SPEC.md section 6 v0.10.0 ledger item 5; AGENTS.md section 3.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) rg -NI --no-heading '\"type\":\"(attachment|last-prompt|permission-mode|mode|ai-title|queue-operation|system|agent-name|file-history-snapshot|file-history-delta|atis-latch|bridge-session|fork-context-ref|cost-state|continued-in)\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'bucket by type (system split by subtype) and count lines carrying a top-level message key' (2) csift search '' <transcript> --no-subagents --count-by label, then the same with -t harness.meta, with -t user, with -T user, and with -t user.queued",
"observed": "0 of 629,829 non-record lines carry a top-level message key, across all 25 non-record type/subtype buckets. Gating, measured on one 158,923-line transcript whose raw line counts are turn_duration 1,005, stop_hook_summary 990, queue-operation 4,177, last-prompt 5,679: a bare --count-by label returns 18 label keys / 42,100 records with NO promoted leaf among them; -t harness.meta returns turn-duration 1,005, stop-hooks 990, snapshot 353, away-summary 342, hook 3; -t user returns only user.message 101 and user.answer 9; -T user alone returns 16 keys with no promoted leaf; -t user.queued returns 20. The turn-duration and stop-hooks census numbers equal the raw line counts exactly (1,005 and 990), so the gated admission is lossless once a selector reaches the leaf.",
"rule": "One observation per physical jsonl line, bucketed by top-level type (system split by subtype); a line counts as carrying message only when the top-level key 'message' exists. Census comparison rule: run the same --count-by label query with and without the selector against one transcript and diff the key sets.",
"note": "The behavior and the gating both reproduce exactly; only the code site needed correction. Two scope notes so the sentence is not over-read. First, 'exactly five' is true only within the queue-and-telemetry scope: 19 further non-record type/subtype buckets in the same census also carry no message key, and they are simply not queue or telemetry facts. Second, the working tree (unreleased, version still 0.10.0 in Cargo.toml) adds a SIXTH gated leaf harness.meta.system for the remaining system subtypes, so 'exactly five promoted leaves' will need re-dating at the next release. Separately observed while running the gating checks and outside this claim: -T user alone left 11 user.unsent rows in the census, which looks inconsistent with the include-minus-exclude contract and is worth its own look."
}
]
},
{
"id": "QT-027",
"area": "queue-and-telemetry",
"behavior": "The presence of a message{} field is the one measured instrument for whether a line is part of the conversation: ZERO of 629,829 lines across the 25 measured non-record type/subtype buckets carries message{}, while every user record (229,721 of 229,721) and every assistant record (432,052 of 432,052) carries it as an object. Claude Code's own internal wording labels the promoted system subtypes render internals - turn_duration is documented as the line the REPL renders as 'Done in Ns' / 'Waiting for N agents'.",
"depends": "csift decides `Class::llm_visible` on that single instrument, which is what keeps a bare role selector (`-t user`) expanding to visible leaves only while the glob form and explicit paths reach the invisible ones.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "183-196",
"snippet": " #[must_use]\n pub fn llm_visible(self) -> bool {\n !matches!(\n self,\n Class::UserUnsent\n | Class::UserQueued\n | Class::CompactionBoundary\n | Class::MetaTurnDuration\n | Class::MetaAwaySummary\n | Class::MetaStopHooks\n | Class::MetaSnapshot\n | Class::MetaSystem\n )\n }"
},
{
"path": "src/cli/selectors.rs",
"lines": "59-72",
"snippet": " if let Some(prefix) = selector.strip_suffix(\".*\") {\n return selector_is_segment_prefix(prefix, path);\n }\n if !selector_is_segment_prefix(selector, path) {\n return false;\n }\n if selector.contains('.') {\n return true; // intermediate prefix or exact leaf: any visibility.\n }\n // A bare role: visible leaves only.\n Class::ALL\n .iter()\n .find(|c| c.path() == path)\n .is_none_or(|c| c.llm_visible())"
},
{
"path": "src/model/taxonomy.rs",
"lines": "172-181",
"snippet": " /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n /// over every non-record line type in the corpus; every user/assistant record\n /// does), and Claude Code's\n /// own source labels them REPL-render internals. DAG threading is NOT the\n /// instrument here - later user records name a `turn_duration` uuid as parentUuid\n /// (chain continuity), and `preservedMessages` lists these uuids at the same rate\n /// as messages (it is a tail window, not a visibility filter).\n ///\n /// A bare ROLE selector (`-t user`) expands to visible leaves only; the"
}
],
"instrument": "`csift stats <target>` prints a whole-file line-type census; for each non user/assistant type, count the lines of that type carrying `\"message\":{`. Counting rule = one observation per physical line, per type; expect 0 for every non-record type.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md section 5; src/model/taxonomy.rs:164-170 comment; SPEC.md section 6 v0.10.0 ledger item 5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) csift stats --format json --max-count 0, summed over the line_types census of all 7,602 session rows (2) rg -NI --no-heading '\"type\":\"(the 15 non-record types)\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'bucket by type/subtype, count lines carrying a top-level message key' (3) rg -NI --no-heading -e '\"type\":\"user\"' -e '\"type\":\"assistant\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'count top-level user/assistant lines and those whose message value is an object' (4) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'From internal SystemMessage'",
"observed": "25 non-record type/subtype buckets over 629,829 lines, ZERO of which carries a top-level message key. Every top-level user line carries a message OBJECT: 229,721 of 229,721. Every assistant line likewise: 432,052 of 432,052. The binary carries the wording verbatim: \"@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the 'Done in Ns' / 'Waiting for N agents' line. From internal SystemMessage 'turn_duration'.\", \"@internal Summary of what happened while the user was away (background tasks completed, notifications accumulated). From internal SystemMessage 'away_summary'.\" and \"@internal Summary of Stop/SubagentStop hook execution at turn end — which hooks ran, their output, and whether any prevented continuation. From internal SystemMessage 'stop_hook_summary'.\" Sibling subtypes carry the same 'REPL renders' phrasing (permission_retry, memory_saved, agents_killed, api_error, scheduled_task_fire).",
"rule": "One observation per physical jsonl line, per bucket. 'Carries message' = the top-level key exists; for the record types the stronger test was used (the value is a JSON object). Counting scope differs slightly between instruments and this matters: csift stats censuses the 7,602 files it enumerates as sessions while the rg pass reads all 7,781 *.jsonl files, which is why the user total reads 229,671 under stats and 229,721 under rg.",
"note": "The instrument is unusually clean - a 0/629,829 versus 661,773/661,773 split with no exception in either direction - so keying llm_visible on it is well founded. Two number corrections: 25 non-record buckets rather than 24 (the corpus has since recorded system/model_refusal_no_fallback and other rare subtypes), and the record totals are several times the claim's 40,490 / 81,024, which look like a single-project scope rather than the corpus. The taxonomy comment should carry its counting scope so the numbers stay checkable."
}
]
},
{
"id": "QT-028",
"area": "queue-and-telemetry",
"behavior": "DAG threading is NOT a visibility instrument: the promoted system telemetry records carry a real uuid and are not marked meta (isMeta:false on 1,005 of 1,005 turn_duration and 342 of 342 away_summary records measured; stop_hook_summary omits the isMeta key entirely on all 5,644), later user records name a turn_duration uuid as their parentUuid (667 in one transcript), and the conversation chain runs straight through a record the model never received.",
"depends": "csift keys visibility on `message{}` presence alone; keying it off parentUuid continuity would flip all five promoted leaves to visible and break the role-selector expansion contract, while the same threading is what makes the records addressable and lets the superseded-draft detector see them as shared parents.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "13-25",
"snippet": " pub fn promoted_class(&self) -> Option<Class> {\n match self.r#type.as_deref()? {\n \"queue-operation\" => self.queued_class(),\n \"system\" => match self.subtype.as_deref()? {\n \"turn_duration\" => Some(Class::MetaTurnDuration),\n \"away_summary\" => Some(Class::MetaAwaySummary),\n \"stop_hook_summary\" => Some(Class::MetaStopHooks),\n // The compaction boundary has its own leaf (`classify`, D7); every OTHER\n // system subtype is the harness talking to its own UI (v0.10.1 catch-all:\n // informational, api_error, model_refusal_*, agents_killed, local_command,\n // scheduled_task_fire, and whatever a later build adds).\n \"compact_boundary\" => None,\n _ => Some(Class::MetaSystem),"
}
],
"instrument": "Take a `turn_duration` uuid from `csift show <target> -t harness.meta.turn-duration --raw`, then `rg '\"parentUuid\":\"<that uuid>\"' over the same file - a user record comes back. Counting rule = one hit per record naming it as parent.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "SPEC.md section 6 v0.10.0 ledger item 5; src/model/taxonomy.rs:164-170 comment; SPEC.md v0.10.0 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c 'over one 158,923-line transcript: collect every uuid of a system/turn_duration, system/stop_hook_summary and system/away_summary record, then count every record whose parentUuid is one of them, bucketed by the child record type; also tabulate isMeta on the three subtypes' - plus the corpus-wide stop_hook_summary key census from QT-021 for the isMeta question",
"observed": "In that transcript: 1,005 turn_duration uuids, 990 stop_hook_summary uuids, 342 away_summary uuids. 667 records of type user name a turn_duration uuid as their parentUuid, so the conversation chain runs straight through a record that carries no message. The full chain is visible: stop_hook_summary -> turn_duration (989 of 990), turn_duration -> user (667) or turn_duration -> away_summary (339), away_summary -> user (342). isMeta is false on 1,005 of 1,005 turn_duration and on 342 of 342 away_summary records, but stop_hook_summary carries NO isMeta key at all - 0 of 5,644 corpus-wide.",
"rule": "One hit per record whose parentUuid equals a collected uuid, bucketed by the child's type/subtype, within a single transcript. isMeta buckets distinguish false, true and key-absent.",
"note": "Strongly confirmed, and the measured chain is richer than the claim states: it is not merely that a user record can name a turn_duration parent, it is that stop_hook_summary, turn_duration and away_summary form a linked run at every turn end, with the next human turn hanging off the tail of it. One correction: 'carry a real uuid with isMeta:false' is true for turn_duration and away_summary but not for stop_hook_summary, which has no isMeta field - which only strengthens the point that isMeta is not the instrument."
}
]
},
{
"id": "QT-029",
"area": "queue-and-telemetry",
"behavior": "Membership in a compaction boundary's compactMetadata.preservedMessages is NOT a visibility instrument either: the field is an OBJECT {anchorUuid, uuids[], allUuids[]}, not a list, and its uuids are an unfiltered TAIL WINDOW of 5 to 11 records anchored on a user record - it lists the promoted telemetry uuids at the same rate as message uuids (telemetry 0.0020-0.0029, message records 0.0014-0.0029, measured over 32 boundaries in one transcript).",
"depends": "csift refuses preservedMessages as a delivery signal and keeps `message{}` presence as the only instrument; reading the list as a visibility filter would mark telemetry as delivered context.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "172-181",
"snippet": " /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n /// over every non-record line type in the corpus; every user/assistant record\n /// does), and Claude Code's\n /// own source labels them REPL-render internals. DAG threading is NOT the\n /// instrument here - later user records name a `turn_duration` uuid as parentUuid\n /// (chain continuity), and `preservedMessages` lists these uuids at the same rate\n /// as messages (it is a tail window, not a visibility filter).\n ///\n /// A bare ROLE selector (`-t user`) expands to visible leaves only; the"
},
{
"path": "src/model/taxonomy.rs",
"lines": "184-196",
"snippet": " pub fn llm_visible(self) -> bool {\n !matches!(\n self,\n Class::UserUnsent\n | Class::UserQueued\n | Class::CompactionBoundary\n | Class::MetaTurnDuration\n | Class::MetaAwaySummary\n | Class::MetaStopHooks\n | Class::MetaSnapshot\n | Class::MetaSystem\n )\n }"
},
{
"path": "src/search/record_text.rs",
"lines": "315-328",
"snippet": "pub(crate) fn compact_metadata_excerpt(meta: &serde_json::Value) -> Option<String> {\n let obj = meta.as_object()?;\n let mut fields: Vec<String> = Vec::new();\n for key in [\"trigger\", \"preTokens\", \"postTokens\", \"durationMs\"] {\n if let Some(v) = obj.get(key) {\n let rendered = match v {\n serde_json::Value::String(s) => s.clone(),\n other => other.to_string(),\n };\n fields.push(format!(\"{key}={rendered}\"));\n }\n }\n (!fields.is_empty()).then(|| format!(\"[compaction boundary: {}]\", fields.join(\" \")))\n}"
},
{
"path": "src/cli/search_args.rs",
"lines": "155-158",
"snippet": " it. Eight leaves are invisible and need naming or a glob: `user.unsent`\\n \\\n (a superseded draft is not in the surviving conversation - CC's own\\n \\\n preservedMessages accounting excludes every draft), `harness.compaction.boundary`\\n \\\n (a metrics-only system record), and the six gated leaves below. The glob form\\n \\"
}
],
"instrument": "Read `compactMetadata.preservedMessages` from a boundary record via `csift show <target> --uuid <boundary uuid> --raw` and compare the uuid mix against the same file line-type census from `csift stats`. Counting rule = uuids per type over one preserved list, reported as a rate against that type total in the file.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.4",
"source": "SPEC.md section 5; src/model/taxonomy.rs:164-170 comment; SPEC.md section 6 v0.10.0 ledger item 5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) python3 -c 'over one 158,923-line transcript: index every uuid to its line type, then for each compact_boundary read compactMetadata.preservedMessages and resolve each listed uuid against that index, reporting per-type counts and each count divided by that type total in the file' (2) rg -NI --no-heading -F '\"subtype\":\"compact_boundary\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'bucket the JSON type of compactMetadata.preservedMessages by record version'",
"observed": "preservedMessages is never a bare list: it is an object on all 232 compact_boundary records in the corpus, shaped {anchorUuid, uuids[], allUuids[]} from 2.1.156 through 2.1.258 (the single 2.1.150 boundary has {anchorUuid, uuids} without allUuids). In the sampled transcript, 32 boundaries, compactMetadata keys = cumulativeDroppedTokens, durationMs, postTokens, preCompactDiscoveredTools, preTokens, preservedMessages, preservedSegment, trigger; preservedSegment = {anchorUuid, headUuid, tailUuid} on 32 of 32. The uuids lists are short tail windows of 5 to 11 entries, 264 entries in total, 243 of them resolvable in the same file, and anchorUuid resolves to a user record on 32 of 32. Per-type preserved counts and their rate against that type's total in the file: attachment 122 of 86,933 = 0.0014, assistant 84 of 28,812 = 0.0029, user 32 of 13,267 = 0.0024, stop_hook_summary 2 of 990 = 0.0020, turn_duration 2 of 1,005 = 0.0020, away_summary 1 of 342 = 0.0029. Telemetry lands in the 0.0020-0.0029 band, message records in the 0.0014-0.0029 band.",
"rule": "One observation per uuid listed in a boundary's preservedMessages.uuids, resolved to a line type by an index of every uuid in the same file; the rate is that per-type count divided by that type's total line count in the same file. Unresolved uuids (21 of 264) are reported separately and excluded from the rates.",
"note": "The conclusion holds and is now quantified with a rerunnable rate rule rather than an assertion. Two wording corrections: preservedMessages is an object, not a list, so the claim's stated instrument (reading it as a list of uuids) would not run as written; and the object has two uuid arrays, uuids and allUuids, which resolved identically here (243 in-file hits each) but differ in length (264 versus 279 entries), so a future reader should say which one it counted. located_csift on this claim is 0.9.4 while the neighbours are 0.10.0."
},
{
"claude_code": "2.1.258",
"csift": "0.10.2",
"date": "2026-09-03",
"verdict": "refined",
"instrument": "count of `Class::llm_visible` arms returning false in model/taxonomy.rs against the search --help visibility sentence",
"observed": "eight leaves return false (user.unsent, harness.compaction.boundary, user.queued, harness.meta.turn-duration, away-summary, stop-hooks, snapshot, system); the 0.10.1 help said exactly two",
"rule": "one false arm = one invisible leaf",
"note": "help sentence corrected in 0.10.2 to name the two plus the six gated leaves"
}
]
},
{
"id": "QT-030",
"area": "queue-and-telemetry",
"behavior": "Claude Code writes five per-turn session-state CACHE line types with NO uuid, NO timestamp, NO parentUuid and NO message: last-prompt (keys leafUuid, sessionId, lastPrompt), permission-mode (permissionMode, sessionId), mode (mode, sessionId), ai-title (aiTitle, sessionId) and agent-name (agentName, sessionId) - each also carries sessionId. They are rewritten per turn - measured last-prompt 29,289, permission-mode 29,229, mode 29,140, ai-title 28,811, agent-name 12,361, 128,830 lines together or 9.98% of a 1,291,324-line corpus - and lastPrompt (itself absent on 87 of 29,289 lines) mostly duplicates text already present as a user.message record: 73% of distinct values verbatim, 90% counting truncated or normalised copies.",
"depends": "csift deliberately leaves them unmodeled and unaddressable by label: they carry no fact `stats` or `list` does not already expose, and a leaf would inject about 28,891 duplicate hits and skew `--count-by label`. They reach only `show --raw` and the `stats` line-type census.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "9-13",
"snippet": " /// The single promoted leaf a NON-message line carries (v0.10.0), or `None` for a\n /// message record and for every line type that stays unmodeled (the session-state\n /// cache lines, the unpromoted system subtypes, a content-less queue `dequeue`).\n #[must_use]\n pub fn promoted_class(&self) -> Option<Class> {"
},
{
"path": "src/show/run.rs",
"lines": "251-262",
"snippet": " if !misses.is_empty() {\n bail!(\n \"no such record(s): {} — an explicit address renders message lines \\\n (role:user/role:assistant, superseded drafts included), attachment lines, and \\\n the promoted non-record lines (a queue-operation with text, turn_duration, \\\n away_summary, stop_hook_summary, file-history-snapshot/-delta, and every \\\n other system subtype such as informational or api_error); session-state \\\n cache lines (last-prompt, mode, ai-title, …), a content-less queue dequeue \\\n and torn lines are inspectable with `--raw`\",\n misses.join(\", \")\n );\n }"
},
{
"path": "src/stats.rs",
"lines": "128-141",
"snippet": "fn line_type_probe(line: &[u8]) -> std::result::Result<Option<String>, ()> {\n if line.iter().all(u8::is_ascii_whitespace) {\n return Ok(None);\n }\n #[derive(serde::Deserialize)]\n struct TypeProbe {\n #[serde(rename = \"type\")]\n r#type: Option<String>,\n }\n match serde_json::from_slice::<TypeProbe>(line) {\n Ok(p) => Ok(Some(p.r#type.unwrap_or_else(|| \"(untyped)\".to_string()))),\n Err(_) => Err(()),\n }\n}"
}
],
"instrument": "`csift stats <project-dir> --format json --max-count 0` and read the whole-file line-type census (counting rule = one observation per LINE, every line syntax-validated); then `rg -m1 '\"type\":\"last-prompt\"' <transcript> | jq 'keys'` shows the absent uuid, timestamp and message.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.1",
"source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 6 v0.10.0 ledger item 8; AGENTS.md section 3.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) csift stats --format json --max-count 0 summed over all session rows for the whole-file line-type census (2) rg -NI --no-heading '\"type\":\"(the 15 non-record types)\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'per-type key-presence counters' (3) csift show <transcript> --line <a last-prompt line>, then the same with --raw (4) python3 -c 'in one transcript, collect distinct non-empty lastPrompt values and test each for verbatim containment in the joined text of that file user records, then for containment of its first 40 characters'",
"observed": "Corpus counts: last-prompt 29,289, permission-mode 29,229, mode 29,140, ai-title 28,811, agent-name 12,361 - 128,830 lines together, 9.98% of the 1,291,324-line corpus. Key sets, with no uuid, no timestamp, no parentUuid and no message on any line: last-prompt {type, leafUuid, sessionId, lastPrompt} where lastPrompt itself is absent on 87 of 29,289; permission-mode {type, permissionMode, sessionId}; mode {type, mode, sessionId}; ai-title {type, aiTitle, sessionId}; agent-name {type, agentName, sessionId}. Reachability: csift show <transcript> --line <N> against a last-prompt line exits non-zero with 'no such record(s): L14', while the identical address with --raw returns the verbatim jsonl line - so show --raw and the stats census are the only surfaces, as claimed. Duplication, over 86 distinct non-empty lastPrompt values in one transcript: 63 (73.3%) occur verbatim inside some user record's text in the same file, a further 14 have their first 40 characters present (89.5% combined), and 9 have no trace at all - and none of those 9 appears in that file's queue-operation content either.",
"rule": "One observation per physical jsonl line for the counts (every line syntax-validated by the stats census). For duplication: one observation per DISTINCT stripped lastPrompt value, tested against the newline-joined text of every user record in the same file; verbatim containment first, then first-40-character containment, then no trace.",
"note": "The decision not to model these holds up: the reachability test reproduces exactly (address errors, --raw returns the line) and the volume argument is if anything stronger at 128,830 lines. One honesty correction: 'merely duplicates' is too strong at the value level - 9 of 86 distinct lastPrompt values in the sampled transcript had no trace anywhere in that file's user records or its queue-operation content, so the cache line is not strictly redundant with the transcript it sits in. That does not change the conclusion (those 9 carry no addressable record, timestamp or uuid, so a leaf would still be unusable), but the claim should say 'mostly duplicates' and name the rate."
}
]
},
{
"id": "QT-031",
"area": "queue-and-telemetry",
"behavior": "Beyond `compact_boundary` and the three promoted subtypes, Claude Code emits exactly seven more `type:\"system\"` subtypes (eleven distinct subtypes in total), at snapshot counts (2026-09-02T10:06:11Z): `scheduled_task_fire` 459 (carrying a `content` render string in 459/459, written beside the delivered wakeup - an `isMeta:true` user record within 2s in 97.4% of cases - and a `queue-operation` enqueue line), `model_refusal_fallback` 51, `api_error` 29 (keys `error`, `retryAttempt`, `maxRetries`, `retryInMs`, `level`), `local_command` 15, `agents_killed` 10 (envelope only: no `content`, no payload key), `informational` 5 and `model_refusal_no_fallback` 2.",
"depends": "the csift classify `system` arm pushes a label only for the compaction boundary plus the three promoted subtypes, so every other subtype yields an empty label vector and is dropped from search results; the `stats` whole-file line-type census stays the enumerating authority, so a new subtype shows up there rather than vanishing silently.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "177-181",
"snippet": " Some(\"system\") => {\n if self.is_compact_boundary() {\n push_unique(&mut out, Class::CompactionBoundary);\n }\n }"
},
{
"path": "src/model/classify_promoted.rs",
"lines": "16-26",
"snippet": " \"system\" => match self.subtype.as_deref()? {\n \"turn_duration\" => Some(Class::MetaTurnDuration),\n \"away_summary\" => Some(Class::MetaAwaySummary),\n \"stop_hook_summary\" => Some(Class::MetaStopHooks),\n // The compaction boundary has its own leaf (`classify`, D7); every OTHER\n // system subtype is the harness talking to its own UI (v0.10.1 catch-all:\n // informational, api_error, model_refusal_*, agents_killed, local_command,\n // scheduled_task_fire, and whatever a later build adds).\n \"compact_boundary\" => None,\n _ => Some(Class::MetaSystem),\n },"
},
{
"path": "src/stats.rs",
"lines": "128",
"snippet": "fn line_type_probe(line: &[u8]) -> std::result::Result<Option<String>, ()> {"
}
],
"instrument": "`cd ~/.claude/projects && rg -oNI -uu --no-messages '\"subtype\":\"[A-Za-z_]+\"' -g '*.jsonl' . | sort | uniq -c`; counting rule = one observation per MATCH. The total must equal the `system` line count from `csift stats --format json --max-count 0` read in the same minute (measured: both 12,712 - independent cross-instrument agreement). The corpus is live, so both numbers rise between runs; only their agreement is stable.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02; SPEC.md section 6 v0.10.0 ledger item 8"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && rg -oNI -uu --no-messages '\"subtype\":\"[A-Za-z_]+\"' -g '*.jsonl' . | sort | uniq -c | sort -rn # then, same minute: csift stats --format json --max-count 0 | tail -1 (read line_types.system)",
"observed": "Snapshot 2026-09-02T10:06:11Z, 11 distinct subtypes: stop_hook_summary 5645, turn_duration 4633, away_summary 1631, scheduled_task_fire 459, compact_boundary 232, model_refusal_fallback 51, api_error 29, local_command 15, agents_killed 10, informational 5, model_refusal_no_fallback 2. Total matches 12712; csift stats line_types.system = 12712 the same minute - exact cross-instrument agreement. The wider needle '\"subtype\":\"[^\"]*\"' also totals 12712, so no subtype value falls outside [A-Za-z_]+. Record shapes (one observation per record, parsed): scheduled_task_fire carries a non-empty string content in 459/459; api_error keys are exactly {error, retryAttempt, maxRetries, retryInMs, level} plus the record envelope; agents_killed carries no content and no payload key beyond the envelope. ZERO of the seven carries a message{} field. Companion test: 447/459 (97.4%) of scheduled_task_fire records have an isMeta:true user record within +/-2s in the same session (455/459 within +/-5s), but only 1/459 has a <task-notification> record within +/-5s. csift 0.10.0 side: `csift search 'Claude resuming /loop wakeup' -t harness -c` = 0 while the text occurs 492 times on disk, so the fire records are indeed dropped from search.",
"rule": "One observation per MATCH of the subtype needle across every *.jsonl under ~/.claude/projects (7,782 files at the snapshot), cross-checked one-observation-per-LINE by the csift stats whole-file line-type census. Counts are a live-corpus snapshot and grow during a session: away_summary moved 1630 -> 1631 between two runs six minutes apart, moving the total 12711 -> 12712. Companion pairing rule: a fire is 'paired' if any record of the stated shape shares its sessionId and lies within the stated window of its timestamp; first match wins, one observation per fire.",
"note": "Claude Code's behaviour is unchanged: the seven extra subtypes are all still emitted, and the subtype set is exactly seven beyond compact_boundary plus the three promoted ones. Three corrections. (1) Two counts moved with the live corpus - model_refusal_fallback 50 -> 51 and informational 1 -> 5 - and the cross-check total 12,577 -> 12,712; the claim should carry a census instant, since the corpus grew twice during this verification alone. (2) The scheduled_task_fire companion was mis-named: it is NOT a schedule-wakeup / task-notification pulse (1/459 within 5s) but an isMeta:true user record carrying the delivered resume prompt (447/459 within 2s), written alongside a queue-operation enqueue line and an attachment; all 459 fires in this corpus render the same content prefix. (3) The csift-side 'depends' is verified for the 0.10.0 binary that ran the instruments (a -t harness search for the fire's render text returns 0 while the text sits on disk 492 times) but is going stale in the working tree, where a v0.10.1 catch-all arm gives every other system subtype the harness.meta.system leaf. One wording nuance on the depends: the stats census keys on the top-level `type`, so a new subtype raises the `system` total rather than being named individually - stats enumerates the bucket, not the subtype."
}
]
},
{
"id": "QT-032",
"area": "queue-and-telemetry",
"behavior": "Claude Code additionally writes at least five line types csift leaves unmodeled. Four carry neither `uuid` nor `timestamp`: `atis-latch` (852, key `atis`), `bridge-session` (788, keys `bridgeSessionId`, `lastSequenceNum`, `ownerAccountUuid`, `ownerOrganizationUuid`), `fork-context-ref` (33, keys `agentId`, `contextLength`, `parentSessionId`, `parentLastUuid`) and `cost-state` (18, keys `totalCostUSD`, `modelUsage`, `startTime`, `hasUnknownModelCost`, `totalAPIDuration`, `totalAPIDurationWithoutRetries`, `totalDuration`, `totalToolDuration`, `totalLinesAdded`, `totalLinesRemoved`). The fifth, `continued-in` (1, keys `continuedInSessionId`, `sessionId`, `timestamp`), does carry a timestamp. Every one of the five also carries `sessionId` except `fork-context-ref`, which names the parent session instead.",
"depends": "csift leaves all four unmodeled; they reach only `show --raw` and the `stats` line-type census. `fork-context-ref` is the only line type that names a fork parent session id and is not yet consumed by the clone-detection path.",
"code": [
{
"path": "src/stats.rs",
"lines": "48-52",
"snippet": " /// Whole-file census: every parseable physical line counted by its top-level `type`\n /// value (`user`, `assistant`, `attachment`, `file-history-snapshot`, …; `(untyped)`\n /// when the field is absent). A FILE fact like `lines`: never windowed by\n /// `--since`/`--turn`.\n line_types: BTreeMap<String, usize>,"
},
{
"path": "src/stats.rs",
"lines": "128",
"snippet": "fn line_type_probe(line: &[u8]) -> std::result::Result<Option<String>, ()> {"
}
],
"instrument": "`csift stats <project-dir> --format json --max-count 0` line-type census; counting rule = one observation per LINE. Cross-check a single type with `rg -cNI '\"type\":\"fork-context-ref\"' -g '*.jsonl'`.",
"located": {
"claude_code": "2.1.258",
"csift": "0.8.1",
"source": "dev session 2026-09-02; SPEC.md section 6 v0.10.0 ledger item 8"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift stats --format json --max-count 0 | tail -1 (read line_types) # keys per type: cd ~/.claude/projects && rg -NI -uu --no-messages '\"type\":\"<t>\"' -g '*.jsonl' . | head -1 | python3 -c 'import sys,json; print(sorted(json.loads(sys.stdin.readline()).keys()))' # reachability: csift show <file> --line N vs csift show <file> --line N --raw",
"observed": "Snapshot 2026-09-02T10:06:11Z line_types over 7,602 scanned transcripts: atis-latch 852, bridge-session 788, fork-context-ref 33, cost-state 18, continued-in 1. Key sets read off a live line of each: atis-latch = [atis, sessionId, type]; bridge-session = [bridgeSessionId, lastSequenceNum, ownerAccountUuid, ownerOrganizationUuid, sessionId, type]; fork-context-ref = [agentId, contextLength, parentLastUuid, parentSessionId, type]; cost-state = [hasUnknownModelCost, modelUsage, sessionId, startTime, totalAPIDuration, totalAPIDurationWithoutRetries, totalCostUSD, totalDuration, totalLinesAdded, totalLinesRemoved, totalToolDuration, type]. None of the four carries `uuid` or `timestamp`. A fifth unmodeled type is present: continued-in (1 line, keys [continuedInSessionId, sessionId, timestamp, type]) - it DOES carry a timestamp. Reachability on an atis-latch line: `show --line` fails with `csift: error: no such record(s): L4 - an explicit address renders message lines ... session-state cache lines ..., a content-less queue dequeue, unpromoted system subtypes and torn lines are inspectable with --raw`, while `show --line --raw` returns the verbatim line (keys atis, sessionId, type).",
"rule": "One observation per LINE: the csift stats whole-file line-type census counts every parseable physical line by its top-level `type`, summed over every session in scope. Key sets are read from the first matching line of each type (the key set was constant across the sampled lines of a type). Live-corpus counts, so they rise between runs.",
"note": "Claude Code's behaviour is unchanged and every key set the claim listed is exactly right - all four confirmed key-for-key, and all four confirmed to carry neither uuid nor timestamp. Two corrections. (1) Three of the four counts moved with the live corpus: atis-latch 450 -> 852, bridge-session 382 -> 788, cost-state 14 -> 18; fork-context-ref held at 33. (2) There is a fifth unmodeled line type, `continued-in`, which the claim does not mention; it is the second type after `fork-context-ref` to name another session (its successor rather than its parent), and unlike the four it carries a timestamp. The depends clause is confirmed directly: a rendered `show --line` address refuses these lines and its error text enumerates what an address does render, while `show --line --raw` returns the verbatim bytes - so `show --raw` plus the stats census really are the only two surfaces that reach them. The observation that `fork-context-ref` names a fork parent and is not consumed by clone detection is a design gap, not a behaviour claim, and no instrument here bears on it."
}
]
},
{
"id": "QT-033",
"area": "queue-and-telemetry",
"behavior": "The internal documentation strings inside Claude Code, extractable from the shipped binary, describe `turn_duration` as the source of the rendered end-of-turn line ('Done in Ns' / 'Waiting for N agents'), `away_summary` as the summary of what happened while the user was away, `scheduled_task_fire` as a cron fire whose `content` is the render text, `agents_killed` as an agents-killed banner and `api_error` as the retry banner - that is, all of them as render internals rather than conversation. The 'five or more minutes away' threshold is real but comes from a separate string documenting the recap setting, corroborated by a 300000 ms constant on the focus-transition path, not from the `away_summary` string itself.",
"depends": "these strings corroborate the `message{}` instrument from the producing side, and they are why csift words the five promoted leaves as `not in the surviving conversation` rather than making the stronger, unprovable claim that the model never saw them.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "181-182",
"snippet": " /// A bare ROLE selector (`-t user`) expands to visible leaves only; the\n /// glob form and explicit paths reach the invisible ones."
}
],
"instrument": "Run `strings -n 6` over the shipped Claude Code binary for the located version and grep for `turn_duration|away_summary|scheduled_task_fire`, then read the surrounding doc comments; counting rule = one observation per extracted string.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o \"@internal[^\\\"]{0,200}(turn_duration|away_summary|scheduled_task_fire|agents_killed|api_error)'\" | sort -u ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '@internal When false, the session recap[^\"]{0,120}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '@internal Retryable-API-error[^\"]{0,220}'",
"observed": "Five doc strings, verbatim: (1) \"@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the 'Done in Ns' / 'Waiting for N agents' line. From internal SystemMessage 'turn_duration'\"; (2) \"@internal Summary of what happened while the user was away (background tasks completed, notifications accumulated). From internal SystemMessage 'away_summary'\"; (3) \"@internal Emitted when a scheduled task (cron) fires. content is the render text. From internal SystemMessage 'scheduled_task_fire'\"; (4) \"@internal Emitted when background agents are terminated (e.g. on interrupt). REPL renders an 'agents killed' banner. From internal SystemMessage 'agents_killed'\"; (5) \"@internal Retryable-API-error frame carrying the plain-data error snapshot and retry counters. REPL renders the retry banner from this. Wire twin is SDKAPIRetryMessage ('api_retry'). From internal SystemMessage 'api_error'.\" The '5+ minutes' figure is in a SIXTH, separate string about the recap setting: \"@internal When false, the session recap (shown when you return after being away for 5+ minutes) is disabled. When absent or true, recap is enabled.\" It is corroborated by the constant 300000 (ms) gating the away path on a focus transition. The away_summary schema in the same binary is `{type:\"system\", subtype:\"away_summary\", content, uuid, session_id}` - no message field. Corpus side: 0 of the 106 sampled records across the seven unmodeled subtypes carries a message{} field. csift wording site: 'not in the surviving conversation' appears at src/model/taxonomy.rs:54, 126 and 131.",
"rule": "One observation per extracted string: a `strings -n 6` dump of the shipped binary, matched by a literal-anchored regex, deduplicated with `sort -u`; a string counts as evidence only if it names the subtype it is being cited for. The message{} check is one observation per record over every record of the seven subtypes in the corpus.",
"note": "Every one of the five doc strings exists verbatim in the 2.1.258 binary and each says what the claim says it says, so the substance holds. One wording correction: the claim attributes the 'five or more minutes' figure to the away_summary documentation, but that string carries no threshold - the figure lives in a separate string about the recap setting, independently corroborated by the 300000 ms constant on the focus-transition gate. Two facts strengthen the claim beyond what it states: the binary's own away_summary schema has no message field, and the api_error string names its SDK wire twin, so the producing side and the message{} instrument agree. The depends clause checks out - the 'not in the surviving conversation' wording is in the source at three sites. The code anchor moved down eight lines; the text is identical."
}
]
},
{
"id": "QT-034",
"area": "queue-and-telemetry",
"behavior": "Claude Code carries an explicit display-only concept for records rendered in the UI but filtered before the API send, and it NEVER lands on disk: the corpus-wide count of the key `\"isVirtual\":` is 0. There is therefore no serialized flag distinguishing a rendered-only record, which is why visibility has to be inferred from the record shape.",
"depends": "csift cannot read a visibility flag off the line and keys on `message{}` presence instead; a future serialized flag would be a stronger instrument than the shape inference.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "181-182",
"snippet": " /// A bare ROLE selector (`-t user`) expands to visible leaves only; the\n /// glob form and explicit paths reach the invisible ones."
}
],
"instrument": "`rg -c '\"isVirtual\":' ~/.claude/projects -g '*.jsonl'` must be 0; counting rule = one observation per matching line.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'is_virtual:[^\"]{0,40}\"@internal Display-only:[^\"]{0,80}' ; cd ~/.claude/projects && rg -oNI -uu --no-messages '\"isVirtual\"' -g '*.jsonl' . | wc -l ; rg -oNI -uu --no-messages '\"is_virtual\"' -g '*.jsonl' . | wc -l ; rg -oNI -uu --no-messages '\"isMeta\"' -g '*.jsonl' . | wc -l",
"observed": "Binary string, verbatim: `is_virtual:x(!0).optional().describe(\"@internal Display-only: rendered in the UI but filtered before API send.` - and its sibling `is_visible_in_transcript_only:x(!0).optional().describe(\"@internal True when the message is stored in the transcript but not rendered in the live UI.` The token appears 24 times in the dump, including predicates that skip virtual records when walking back for the last assistant message. Corpus: `\"isVirtual\"` = 0 occurrences and `\"is_virtual\"` = 0 occurrences across 7,782 *.jsonl files; control key `\"isMeta\"` = 9,074 occurrences in the same scan.",
"rule": "One observation per MATCH over every *.jsonl under ~/.claude/projects, needle taken WITHOUT the trailing colon (a strictly weaker needle than the claim's `\"isVirtual\":`, so a zero here implies a zero there). The control needle `\"isMeta\"` is counted in the identical scan to prove the scan reaches record interiors rather than silently matching nothing.",
"note": "Both halves confirmed, and the binary string is almost word-for-word the claim: Claude Code documents the flag as 'Display-only: rendered in the UI but filtered before API send', which is exactly the display-only concept the claim asserts. On disk the flag is absent - zero occurrences in either the camelCase or the wire spelling over 7,782 transcripts, against a control key that matched 9,074 times in the same pass - so the shape inference csift relies on is still the only available instrument. One scope caveat a stranger should keep: the binary does contain a path that copies `is_virtual` from an SDK input message onto an internal record, so a session driven through the SDK with that field set is the case that could put the flag on disk; nothing in this corpus exercises it, and a corpus containing such a session would decide whether 'never lands on disk' is universal or merely true of interactive sessions. The claim's code anchor moved down eight lines; the quoted text is unchanged."
}
]
},
{
"id": "QT-035",
"area": "queue-and-telemetry",
"behavior": "The promoted queue and telemetry lines are high-volume duplicates of records already labelled elsewhere: admitting the queue lines alone would add 13,514 lines to a default scan surface, of which 9,276 carry content and 87.4% of those (8,106) are automation pulses whose delivered twin is already searchable. Admitting all five promoted leaves would add 31,470 lines and 19,125 labelled records.",
"depends": "csift gates each promoted leaf behind an explicit selector or a `show` address through `CandidateGates` - one `&&`-gated SIMD memmem per leaf, so a default scan pays nothing. The gate is STRICTER than the compaction-boundary keep, which a match-all scan admits while these leaves stay out.",
"code": [
{
"path": "src/search/scan.rs",
"lines": "312-319",
"snippet": "pub(crate) struct CandidateGates {\n /// D7: the `compact_boundary` metrics record (selected label).\n pub(crate) compact_boundary: bool,\n /// `--additional-context` (or an address): hook-injected context attachments.\n pub(crate) hook_context: bool,\n /// `--attachments` / the attachment axis (or an address): every attachment line.\n pub(crate) attachments: bool,\n /// v0.10.0 (explicit selector or address): `queue-operation` lines."
},
{
"path": "src/search/scan.rs",
"lines": "338-343",
"snippet": "pub(crate) fn line_is_transcript_candidate(line: &[u8], gates: &CandidateGates) -> bool {\n let needs_compact_boundary = gates.compact_boundary;\n let needs_hook_context = gates.hook_context;\n let needs_attachments = gates.attachments;\n // Every user/assistant record carries a `\"role\":\"user\"`/`\"role\":\"assistant\"`\n // marker (genuine-user string content, tool carriers, assistant blocks all do)."
}
],
"instrument": "Compare `csift search '' --count-by label` with `csift search '' -t 'user.*' --count-by label` and `csift search '' -t harness.meta --count-by label`: the gated leaves are absent from the first and present in the others. Counting rule = matched records per leaf per invocation; a zero-match run with no reaching selector emits the stderr gated-leaves note and the JSON `gated_leaves_unreached` key. (The claim's `-c` form counts EXCHANGES, not records, so it cannot show a per-leaf admission.)",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "AGENTS.md section 3.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' --count-by label vs csift search '' -t 'user.*' --count-by label vs csift search '' -t harness.meta --count-by label ; csift search '<absent token>' -t user.message --format json ; cd ~/.claude/projects && rg -NI -uu --no-messages '\"type\":\"queue-operation\"' -g '*.jsonl' . | python3 (classify each line's content by section-boundary tag)",
"observed": "Default scan (no -t) emits 27 label keys and NONE of the gated leaves: no user.queued, no harness.meta.turn-duration, no harness.meta.away-summary, no harness.meta.stop-hooks, no harness.meta.snapshot. With -t 'user.*': user.queued appears at 1,170 records. With -t harness.meta: snapshot 6,047, stop-hooks 5,645, turn-duration 4,633, away-summary 1,630 appear. A zero-match run under a non-reaching selector returns JSON `\"gated_leaves_unreached\": true` alongside `\"definitive_absence\": true`, and stderr prints: `csift: note: the gated leaves (user.queued, harness.meta.turn-duration, harness.meta.away-summary, harness.meta.stop-hooks, harness.meta.snapshot) are scanned only under an explicit -t that reaches them - this absence does not cover those lines.` Volume: 13,514 queue-operation lines corpus-wide (NOT ~22,000), of which 9,276 carry non-empty string content and 4,238 do not; of the content-bearing lines 8,106 are automation pulses (8,076 `<task-notification>` + 30 `<agent-message>`) = 87.4%, and 1,170 are the human's text = 12.6%, matching the user.queued census exactly. Operation field: enqueue 6,761, dequeue 3,442, remove 3,203, popAll 108.",
"rule": "Gating: one observation per RECORD via `--count-by label`, run three times over the same whole-corpus scope, changing only the -t selector; a leaf is 'gated' iff it is absent from the no-selector census and present under a reaching selector. Volume: one observation per LINE of `type:\"queue-operation\"`; a line is an automation pulse iff its `content`, after left-trimming, STARTS with `<task-notification>` or `<agent-message` (a section-boundary test, so a mid-prose quotation does not count). Percentages are over content-bearing lines only.",
"note": "The gating mechanism is confirmed exactly as described, by the strongest available instrument: three label censuses over one scope differing only in the selector show the five promoted leaves absent by default and present under a reaching -t, and the zero-match diagnosis names all five leaves on stderr and sets gated_leaves_unreached in JSON. The 87% figure is right to the decimal - 8,106 of 9,276 content-bearing queue lines are automation pulses, 87.4% - and the 1,170 human-content remainder matches the user.queued census exactly, an independent cross-check. One number is wrong: 'roughly 22,000' queue lines overstates by 63%; the corpus holds 13,514 queue-operation lines, and no reading recovers 22,000 (all five promoted leaves together come to 31,470 lines). Worth recording alongside it: a queue line is written on enqueue AND on dequeue/remove, so a single pulse is counted about twice, which is part of why the queue duplicates so heavily. The claim's own suggested instrument uses -c, which counts exchanges rather than records and so cannot demonstrate the gating; the corrected instrument uses --count-by label."
}
]
},
{
"id": "QT-036",
"area": "queue-and-telemetry",
"behavior": "An assistant record's `message.usage` object carries eleven keys - `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, `service_tier`, `cache_creation`, `inference_geo`, `server_tool_use`, `iterations`, `speed`, `output_tokens_details` - and Claude Code 2.1.258 emits all eleven on every assistant record: 1,424 of 1,424 records written by 2.1.258 carry all eleven. Presence rates below 100% are a fossil of OLDER Claude Code versions in the same corpus: over a mixed-version 15-transcript sample (12,514 records) the first seven keys are on 100%, `server_tool_use`/`iterations`/`speed` on 12,478 (99.71%), and `output_tokens_details` on only 1,919 (15.3%). PRESENT is not the same as CARRIES A NUMBER: 2.1.258's own schema declares `cache_creation_input_tokens`, `cache_read_input_tokens`, `server_tool_use`, `service_tier` and `cache_creation` nullable, and `inference_geo`, `speed` and `output_tokens_details` nullable AND optional (`iterations` optional), and the corpus shows null values for five of them. Value shapes: the four token counts are integers, `service_tier` / `speed` / `inference_geo` are strings, `cache_creation` / `server_tool_use` / `output_tokens_details` are objects, and `iterations` is an ARRAY of per-API-call usage objects.",
"depends": "csift's `TokenUsage` probe deserializes exactly four of the eleven (`input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`), so `csift stats` reports no thinking-token split, no server-tool counts, no service tier and no inference geography; adding any of them means reading a key whose absence rate is already measured.",
"code": [
{
"path": "src/model/record.rs",
"lines": "270-275",
"snippet": "pub struct TokenUsage {\n pub input_tokens: Option<u64>,\n pub output_tokens: Option<u64>,\n pub cache_read_input_tokens: Option<u64>,\n pub cache_creation_input_tokens: Option<u64>,\n}"
},
{
"path": "src/model/record.rs",
"lines": "281-284",
"snippet": " pub fn token_usage(&self) -> Option<TokenUsage> {\n let raw = self.usage.as_ref()?;\n serde_json::from_str(raw.get()).ok()\n }"
}
],
"instrument": "Two instruments. (1) `strings -n 6` over the 2.1.258 binary for the literal usage schema and the literal zero-value usage template - both name exactly eleven keys. (2) Python over the transcript corpus: count each usage key across assistant records, once per record, reported separately for the version-current subset (records whose `version` is 2.1.258) and for a mixed-version sample.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'usage:c\\(\\{input_tokens:A\\(\\).{0,420}' | sort -u AND python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects'); VER=b'\"version\":\"2.1.258\"'\nkeys=collections.Counter(); nested=collections.defaultdict(collections.Counter); n=0\nfor p in glob.glob(os.path.join(root,'*','*.jsonl')):\n for raw in open(p,'rb'):\n if VER not in raw or b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant' or r.get('version')!='2.1.258': continue\n u=(r.get('message') or {}).get('usage')\n if not isinstance(u,dict): continue\n n+=1\n for k in u: keys[k]+=1\n for o in ('cache_creation','server_tool_use','output_tokens_details'):\n if isinstance(u.get(o),dict):\n for k in u[o]: nested[o][k]+=1\nprint(n, keys.most_common(), {k:dict(v) for k,v in nested.items()})\nPY AND python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\nkeys=collections.Counter(); types=collections.defaultdict(collections.Counter); n=0\nfor p in sample:\n for raw in open(p,'rb'):\n if b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant': continue\n u=(r.get('message') or {}).get('usage')\n if not isinstance(u,dict): continue\n n+=1\n for k,v in u.items(): keys[k]+=1; types[k][type(v).__name__]+=1\nprint(n, keys.most_common(), {k:dict(v) for k,v in types.items()})\nPY",
"observed": "Binary schema literal (one unique hit): usage:c({input_tokens:A(),output_tokens:A(),cache_creation_input_tokens:A().nullable(),cache_read_input_tokens:A().nullable(),server_tool_use:c({web_search_requests:A(),web_fetch_requests:A()}).nullable(),service_tier:i().nullable(),cache_creation:c({ephemeral_1h_input_tokens:A(),ephemeral_5m_input_tokens:A()}).nullable(),inference_geo:i().nullable().optional(),speed:i().nullable().optional(),iterations:de().optional(),output_tokens_details:c({thinking_tokens:A().nullable().optional()}).nullable().optional()}) -- exactly 11 keys, the same 11 the claim names. A second literal gives the zero-value runtime template: output_tokens_details:{thinking_tokens:0},input_tokens:0,cache_creation_input_tokens:0,cache_read_input_tokens:0,output_tokens:0,server_tool_use:{web_search_requests:0,web_fetch_requests:0},service_tier:\"standard\",cache_creation:{ephemeral_1h_input_tokens:0,ephemeral_5m_input_tokens:0},inference_geo:\"\",iterations:[],speed:\"standard\". Corpus, version-current subset: 1,424 records over 5 transcripts, and ALL ELEVEN keys present on 1,424/1,424 (100%), output_tokens_details included. Corpus, mixed-version 15-file sample: 12,514 records; input_tokens / output_tokens / cache_read_input_tokens / cache_creation_input_tokens / service_tier / cache_creation / inference_geo = 12,514 (100%); server_tool_use / iterations / speed = 12,478 (99.71%); output_tokens_details = 1,919 (15.3%). Value types: input/output/cache_* int, service_tier / inference_geo / speed string, cache_creation / server_tool_use / output_tokens_details object, iterations ARRAY (of per-API-call usage objects). Null-VALUED occurrences in the mixed sample: service_tier 38, inference_geo 38, speed 38, iterations 38, output_tokens_details 1.",
"rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). VERSION-CURRENT SUBSET = every line in every top-level transcript under ~/.claude/projects/*/*.jsonl whose raw bytes contain '\"version\":\"2.1.258\"', re-checked after parse as record.version == \"2.1.258\", type == \"assistant\", message.usage a JSON object. One observation per record. A key counts as PRESENT when it is a key of the usage object, whether or not its value is null.",
"note": "Code sites re-checked: src/model/record.rs:269-274 and :280-283 contain the claimed snippets verbatim at the claimed lines, so `TokenUsage` still deserializes four of the eleven keys and csift still reports no thinking split, no server-tool counts, no service tier and no inference geography. The claim's presence rates are correct for the corpus it sampled but are NOT a statement about current Claude Code: on 2.1.258 the rate is 100% for all eleven, so `output_tokens_details` is no longer a 6.4% rarity but a field a reader will meet on every record."
}
]
},
{
"id": "QT-037",
"area": "queue-and-telemetry",
"behavior": "Three of the `message.usage` values are nested objects with fixed shapes, not flat numbers: `cache_creation` carries `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens`, `server_tool_use` carries `web_search_requests` and `web_fetch_requests`, and `output_tokens_details` carries `thinking_tokens`. The first two held on every record measured (12,514/12,514 and 12,478/12,478 in a mixed-version 15-transcript sample; 1,424/1,424 each on records written by 2.1.258). `output_tokens_details` is the one that can be absent as an object: it read NULL rather than an object on 1 of 1,919 records, and 2.1.258's own schema declares `thinking_tokens` nullable AND optional inside an `output_tokens_details` that is itself nullable and optional - so a reader must tolerate a null wrapper and a missing inner key, not just a missing outer key. Where the object was present its key set was exactly {thinking_tokens} on all 1,918 occurrences.",
"depends": "csift reads none of the nested objects, so `stats` cannot report the 5m/1h cache split, web-tool call counts, or a thinking/non-thinking output split; a future addition must know these are nested, not flat, and that the flat `cache_creation_input_tokens` is a separate sibling key.",
"code": [
{
"path": "src/model/record.rs",
"lines": "281-284",
"snippet": " pub fn token_usage(&self) -> Option<TokenUsage> {\n let raw = self.usage.as_ref()?;\n serde_json::from_str(raw.get()).ok()\n }"
},
{
"path": "src/model/record.rs",
"lines": "270-275",
"snippet": "pub struct TokenUsage {\n pub input_tokens: Option<u64>,\n pub output_tokens: Option<u64>,\n pub cache_read_input_tokens: Option<u64>,\n pub cache_creation_input_tokens: Option<u64>,\n}"
}
],
"instrument": "Two instruments. (1) `strings -n 6` over the 2.1.258 binary for the nested usage schema and the zero-value usage template. (2) The same Python sweep over assistant records carrying `message.usage`, additionally iterating the keys of `usage['cache_creation']`, `usage['server_tool_use']` and `usage['output_tokens_details']` when each is a dict, and counting the non-dict (null) occurrences separately.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'output_tokens_details:c\\(\\{thinking_tokens:.{0,60}' | sort -u AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'output_tokens_details:\\{thinking_tokens:0\\},input_tokens:0,.*speed:\"standard\"' | sort -u AND python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects'); VER=b'\"version\":\"2.1.258\"'\nkeys=collections.Counter(); nested=collections.defaultdict(collections.Counter); n=0\nfor p in glob.glob(os.path.join(root,'*','*.jsonl')):\n for raw in open(p,'rb'):\n if VER not in raw or b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant' or r.get('version')!='2.1.258': continue\n u=(r.get('message') or {}).get('usage')\n if not isinstance(u,dict): continue\n n+=1\n for k in u: keys[k]+=1\n for o in ('cache_creation','server_tool_use','output_tokens_details'):\n if isinstance(u.get(o),dict):\n for k in u[o]: nested[o][k]+=1\nprint(n, keys.most_common(), {k:dict(v) for k,v in nested.items()})\nPY",
"observed": "Binary: output_tokens_details:c({thinking_tokens:A().nullable().optional()}).nullable().optional() -- the inner `thinking_tokens` is BOTH nullable and optional inside a wrapper that is itself nullable and optional. Binary zero template: cache_creation:{ephemeral_1h_input_tokens:0,ephemeral_5m_input_tokens:0}, server_tool_use:{web_search_requests:0,web_fetch_requests:0}, output_tokens_details:{thinking_tokens:0} -- the three nested shapes exactly as claimed. Corpus, version-current subset (1,424 records): cache_creation carried ephemeral_1h_input_tokens and ephemeral_5m_input_tokens on 1,424/1,424; server_tool_use carried web_search_requests and web_fetch_requests on 1,424/1,424; output_tokens_details was an object on 1,423 and NULL on 1, and every one of the 1,423 objects carried thinking_tokens. Mixed-version 15-file sample: cache_creation sub-keys 12,514/12,514, server_tool_use sub-keys 12,478/12,478, output_tokens_details object on 1,918 (plus 1 null) whose key SET was exactly ('thinking_tokens',) on all 1,918.",
"rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). VERSION-CURRENT SUBSET = every line in every top-level transcript under ~/.claude/projects/*/*.jsonl whose raw bytes contain '\"version\":\"2.1.258\"', re-checked after parse as record.version == \"2.1.258\", type == \"assistant\", message.usage a JSON object. One observation per record. One observation per (record, nested key) pair; a nested value that is null rather than an object contributes no nested-key observations and is counted separately.",
"note": "Code sites re-checked: src/model/record.rs:269-274 and :280-283 contain the claimed snippets verbatim at the claimed lines, so csift still reads none of the nested objects. The claim's 'on all 456' for `thinking_tokens` over-states universality - the field is optional and nullable by schema, and one null wrapper turned up in 1,919 records. The rest of the claim reproduced exactly, and the binary's literal zero-value template is a stronger witness than any sample because it enumerates the shape rather than a frequency."
}
]
},
{
"id": "QT-038",
"area": "queue-and-telemetry",
"behavior": "`cache_creation.ephemeral_5m_input_tokens + cache_creation.ephemeral_1h_input_tokens` equals the flat `cache_creation_input_tokens` on 12,428 of 12,514 records (99.31%) in a mixed-version 15-transcript sample and disagrees on 86 (0.69%); on records written by Claude Code 2.1.258 it agreed on 1,424 of 1,424 (100%). The nested pair therefore very nearly partitions the flat field but is not guaranteed to, and the disagreement has a shape: all 86 involved a non-zero `ephemeral_1h_input_tokens` that the flat field did not account for (55 with a flat 0, 31 with a flat above 0 but a zero 5m). Claude Code's own code makes the independence explicit - it takes the API's flat value when present and only falls back to the nested sum when it is absent, and elsewhere picks the flat value when above 0 and the nested sum otherwise, never adding the two.",
"depends": "Summing the nested pair alongside the flat field would double-count cache-creation tokens in `csift stats` on 99% of records; a future implementation must pick one or the other, and must not assume the identity holds on every record.",
"code": [
{
"path": "src/stats.rs",
"lines": "258-263",
"snippet": " let vals = [\n u.input_tokens.unwrap_or(0),\n u.output_tokens.unwrap_or(0),\n u.cache_read_input_tokens.unwrap_or(0),\n u.cache_creation_input_tokens.unwrap_or(0),\n ];"
}
],
"instrument": "Python over the mixed-version sample and over the version-current subset: for each assistant record with a dict `usage.cache_creation`, compare `(ephemeral_5m_input_tokens or 0) + (ephemeral_1h_input_tokens or 0)` against `(cache_creation_input_tokens or 0)`, and bucket the disagreements by which of the three is zero; plus `strings -n 6` over the 2.1.258 binary for the fallback expression and the pick-one expression.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\neq=ne=tot=0; shapes=collections.Counter()\nfor p in sample:\n for raw in open(p,'rb'):\n if b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant': continue\n u=(r.get('message') or {}).get('usage')\n if not isinstance(u,dict): continue\n cc=u.get('cache_creation')\n if not isinstance(cc,dict): continue\n tot+=1\n a=cc.get('ephemeral_5m_input_tokens') or 0; b=cc.get('ephemeral_1h_input_tokens') or 0\n flat=u.get('cache_creation_input_tokens') or 0\n if a+b==flat: eq+=1\n else:\n ne+=1\n shapes[('flat=0' if flat==0 else 'flat>0','5m=0' if a==0 else '5m>0','1h=0' if b==0 else '1h>0')]+=1\nprint(tot,eq,ne,shapes.most_common())\nPY AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'cache_creation_input_tokens:n\\.cache_creation_input_tokens\\?\\?.{0,120}' | sort -u AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '_=r\\?\\.cache_creation_input_tokens\\?\\?0,v=\\(r\\?\\.cache_creation\\?\\.ephemeral_5m_input_tokens\\?\\?0\\)\\+\\(r\\?\\.cache_creation\\?\\.ephemeral_1h_input_tokens\\?\\?0\\),C=_>0\\?_:v' | sort -u",
"observed": "Mixed-version 15-transcript sample: 12,514 records carry a dict `cache_creation`; the nested pair sums to the flat field on 12,428 (99.31%) and disagrees on 86 (0.69%). EVERY disagreement had ephemeral_1h_input_tokens > 0 - 55 with flat == 0 and 5m == 0, 31 with flat > 0 and 5m == 0 - i.e. the flat field never tracked a 1-hour cache write in those records. Version-current subset: 1,424 of 1,424 records agree (100%), 0 disagreements. Binary: cache_creation_input_tokens:n.cache_creation_input_tokens??(r?(r.ephemeral_1h_input_tokens??0)+(r.ephemeral_5m_input_tokens??0):e.cache_creation_input_tokens) -- the flat field is the API's own value when present and only FALLS BACK to the nested sum, so the identity is not enforced anywhere. Binary, the pick-one rule stated in code: _=r?.cache_creation_input_tokens??0,v=(r?.cache_creation?.ephemeral_5m_input_tokens??0)+(r?.cache_creation?.ephemeral_1h_input_tokens??0),C=_>0?_:v",
"rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). VERSION-CURRENT SUBSET = every line in every top-level transcript under ~/.claude/projects/*/*.jsonl whose raw bytes contain '\"version\":\"2.1.258\"', re-checked after parse as record.version == \"2.1.258\", type == \"assistant\", message.usage a JSON object. One observation per record. One observation per record with a dict `usage.cache_creation`; missing sub-keys treated as 0; EQUAL means (5m or 0) + (1h or 0) == (cache_creation_input_tokens or 0).",
"note": "Code site re-checked: src/stats.rs:258-263 contains the claimed four-element `vals` array verbatim at the claimed lines, so csift still sums only the flat field and cannot double-count today. The claim's direction and its warning are both right; only the percentages moved (99.08/0.92 claimed, 99.31/0.69 measured on a different sample) and the disagreement now has a named shape - it is the 1-hour cache TTL, and it did not occur at all in the 2.1.258 records."
}
]
},
{
"id": "QT-039",
"area": "queue-and-telemetry",
"behavior": "`output_tokens_details.thinking_tokens` is a SUBSET of `output_tokens`, not an addition to it - but only where `output_tokens` carries a real number. Over a mixed-version 15-transcript sample, 1,916 of 1,918 records carrying the key satisfied thinking_tokens <= output_tokens, and among the 1,916 records whose output_tokens is above 0 there were 0 violations and 0 ties. The 2 violations were records whose four top-level token counts had all been zeroed while the nested fields kept their values, so the comparison was against a 0 that is an artifact rather than a real output count. On records written by Claude Code 2.1.258 the relation held on all 1,423 that carry the key.",
"depends": "If `csift stats` ever reports a thinking split it must SUBTRACT rather than add, otherwise the per-model output total would exceed what the API billed; the measured direction is what makes that safe to assert.",
"code": [
{
"path": "src/stats.rs",
"lines": "307",
"snippet": " for ((model, _), vals) in usage_peak {"
},
{
"path": "src/stats.rs",
"lines": "258-263",
"snippet": " let vals = [\n u.input_tokens.unwrap_or(0),\n u.output_tokens.unwrap_or(0),\n u.cache_read_input_tokens.unwrap_or(0),\n u.cache_creation_input_tokens.unwrap_or(0),\n ];"
}
],
"instrument": "Python over a mixed-version 15-transcript sample and over the version-current subset: for each assistant record with a dict `usage.output_tokens_details` carrying `thinking_tokens`, compare it against `usage.output_tokens`; report the <= and > counts BOTH over all such records and over the sub-population with output_tokens > 0, so a zeroed record cannot masquerade as a counterexample.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY'\nimport os,glob,json\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\ntot=le=gt=posout=posviol=0\nfor p in sample:\n for raw in open(p,'rb'):\n if b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant': continue\n u=(r.get('message') or {}).get('usage')\n if not isinstance(u,dict): continue\n d=u.get('output_tokens_details')\n if not (isinstance(d,dict) and 'thinking_tokens' in d): continue\n tot+=1; t=d.get('thinking_tokens') or 0; o=u.get('output_tokens') or 0\n le+= t<=o; gt+= t>o\n if o>0:\n posout+=1; posviol+= t>o\nprint(tot,le,gt,posout,posviol)\nPY AND python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects'); VER=b'\"version\":\"2.1.258\"'\nkeys=collections.Counter(); nested=collections.defaultdict(collections.Counter); n=0\nfor p in glob.glob(os.path.join(root,'*','*.jsonl')):\n for raw in open(p,'rb'):\n if VER not in raw or b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant' or r.get('version')!='2.1.258': continue\n u=(r.get('message') or {}).get('usage')\n if not isinstance(u,dict): continue\n n+=1\n for k in u: keys[k]+=1\n for o in ('cache_creation','server_tool_use','output_tokens_details'):\n if isinstance(u.get(o),dict):\n for k in u[o]: nested[o][k]+=1\nprint(n, keys.most_common(), {k:dict(v) for k,v in nested.items()})\nPY",
"observed": "Mixed-version 15-transcript sample: 1,918 records carry `output_tokens_details.thinking_tokens`; thinking_tokens <= output_tokens on 1,916 and thinking_tokens > output_tokens on 2. Restricting to the 1,916 records whose `output_tokens` is above 0: 0 violations, and 0 records where the two were equal. The 2 violating records are the two per-block copies of ONE API message sitting immediately after a record with isCompactSummary true; their usage reads input_tokens 0, output_tokens 0, cache_read_input_tokens 0, cache_creation_input_tokens 0 while output_tokens_details.thinking_tokens is 2218, cache_creation.ephemeral_1h_input_tokens is 2140 and the `iterations` array still carries the real numbers (input_tokens 2, output_tokens 2973, cache_read_input_tokens 962321, cache_creation_input_tokens 2140). Version-current subset: 1,423 records carry the key, 1,423 satisfied <=, 0 violations.",
"rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). VERSION-CURRENT SUBSET = every line in every top-level transcript under ~/.claude/projects/*/*.jsonl whose raw bytes contain '\"version\":\"2.1.258\"', re-checked after parse as record.version == \"2.1.258\", type == \"assistant\", message.usage a JSON object. One observation per record. One observation per record whose `usage.output_tokens_details` is a dict carrying `thinking_tokens`; missing values treated as 0; reported both over all such records and over the sub-population with output_tokens > 0.",
"note": "Code sites re-checked: src/stats.rs:307 and :258-263 contain the claimed snippets verbatim at the claimed lines. The claim's 'with 0 violations' does not survive a larger sample - 2 of 1,918 records violate it - but the violations are exactly the zeroed-usage shape QT-041 describes, so the engineering conclusion is unchanged and in fact sharpened: a subtraction is safe, and a future implementation should guard it against a zeroed output_tokens rather than trusting an unconditional inequality."
}
]
},
{
"id": "QT-040",
"area": "queue-and-telemetry",
"behavior": "Claude Code repeats the IDENTICAL `message.usage` object on every per-block record of ONE API message, keyed by a shared `message.id`: over a 15-transcript sample, 3,864 of the 3,865 message.ids appearing on more than one record had every copy byte-identical, and the duplication persists at 2.1.258 (1,424 usage records over 624 distinct message.ids, a mean of 2.28 copies and a maximum of 9). A per-record sum therefore over-reports the true totals, by a factor that depends on which field is summed: over the four fields together the 15 transcripts measured 1.91x to 3.17x with a median of 2.64x, while `input_tokens` alone reached 4.73x on the worst file.",
"depends": "`csift stats` dedupes per FILE by `message.id`, taking the per-field MAX across that id's admitted records, and a record with no `message.id` counts on its own; the scope TOTAL then sums files, so the counting rule differs between a per-file figure and a scope figure.",
"code": [
{
"path": "src/stats.rs",
"lines": "250-256",
"snippet": " // CC repeats the IDENTICAL message.usage on every per-block record of\n // one API message; summing per record over-reports 2.2-3.5x (measured).\n // Dedupe per FILE by message.id, taking the per-field MAX across the\n // id's admitted records: identical on clean data, and immune to the\n // compaction-replay shape where a replayed copy carries ZEROED usage\n // (first-wins would depend on traversal order). An id-less record\n // counts on its own, as before."
},
{
"path": "src/stats.rs",
"lines": "258-263",
"snippet": " let vals = [\n u.input_tokens.unwrap_or(0),\n u.output_tokens.unwrap_or(0),\n u.cache_read_input_tokens.unwrap_or(0),\n u.cache_creation_input_tokens.unwrap_or(0),\n ];"
},
{
"path": "src/stats.rs",
"lines": "307",
"snippet": " for ((model, _), vals) in usage_peak {"
}
],
"instrument": "`csift stats @<id> --format json` versus a naive per-record sum of `message.usage.input_tokens` over the same file; the ratio is the over-read factor. Counting rule: naive = one addition per assistant record carrying usage; csift = one addition per distinct `message.id` per file.",
"located": {
"claude_code": null,
"csift": "0.9.2",
"source": "AGENTS.md section 1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY'\nimport os,glob,json,statistics\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]; out=[]\nfor p in sample:\n naive=[0]*4; peak={}; n=0\n for raw in open(p,'rb'):\n if b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant': continue\n m=r.get('message') or {}; u=m.get('usage')\n if not isinstance(u,dict): continue\n n+=1\n v=[u.get('input_tokens') or 0,u.get('output_tokens') or 0,\n u.get('cache_read_input_tokens') or 0,u.get('cache_creation_input_tokens') or 0]\n naive=[a+b for a,b in zip(naive,v)]\n k=(m.get('model'),m['id']) if m.get('id') else ('none',n)\n peak[k]=[max(a,b) for a,b in zip(peak.get(k,[0]*4),v)]\n ded=[sum(x[i] for x in peak.values()) for i in range(4)]\n out.append((n,len(peak),round(sum(naive)/max(sum(ded),1),2),round(naive[0]/max(ded[0],1),2)))\nprint(out); r=[o[2] for o in out]\nprint('all4 min/median/max', min(r), statistics.median(r), max(r), 'input max', max(o[3] for o in out))\nPY AND python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\nmulti=ident=diff=mixed=0\nfor p in sample:\n g=collections.defaultdict(list)\n for i,raw in enumerate(open(p,'rb'),1):\n if b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant': continue\n m=r.get('message') or {}; u=m.get('usage')\n if not isinstance(u,dict) or not m.get('id'): continue\n g[m['id']].append((i,json.dumps(u,sort_keys=True)))\n for mid,rows in g.items():\n if len(rows)<2: continue\n multi+=1; s={x[1] for x in rows}\n if len(s)==1: ident+=1\n else:\n diff+=1\n t=[json.loads(x) for x in s]\n z=[x for x in t if not any((x.get(k) or 0) for k in\n ('input_tokens','output_tokens','cache_read_input_tokens','cache_creation_input_tokens'))]\n if z and len(z)<len(t): mixed+=1; print('differing group at lines', [x[0] for x in rows])\nprint('multi',multi,'identical',ident,'differing',diff,'zero+nonzero',mixed)\nPY AND csift stats <one transcript path under ~/.claude/projects> --no-subagents --format json",
"observed": "Duplication is real and byte-exact: over the 15-transcript sample, 3,865 message.ids appear on more than one record, and on 3,864 of them (99.97%) every copy's whole usage object is byte-identical after canonical JSON serialization; exactly 1 differs (the QT-041 compaction-replay case). Multiplicity persists in the current version: the 1,424 records written by 2.1.258 that carry usage span 624 distinct (file, message.id) pairs - a mean of 2.28 records per API message, maximum 9 copies. Over-read factor, per transcript, naive sum divided by the deduped sum: on the four fields summed together the 15 files gave min 1.91, median 2.64, max 3.17; taking input_tokens alone the same files reached 4.73. Cross-check that csift implements the stated rule: on one transcript `csift stats --no-subagents --format json` reported, summed across the models in its `tokens` map, input 664, output 530,169, cache_read 93,433,115, cache_creation 16,454,008 - identical in all four fields to an independent Python per-(model, message.id) MAX reimplementation, against a naive per-record sum on the same file of input 3,141, output 2,216,048, cache_read 293,906,300, cache_creation 53,836,886.",
"rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). NAIVE = one addition per assistant record carrying usage. DEDUPED = one addition per distinct (model, message.id) pair per file, taking the per-field MAX across that pair's records; a record with no message.id counts on its own. RATIO = naive divided by deduped, computed per file, reported over the four fields summed and for input_tokens alone.",
"note": "Code sites re-checked: src/stats.rs:250-256, :258-263 and :307 contain the claimed snippets verbatim at the claimed lines. The mechanism and csift's counting rule both verified - an independent reimplementation of per-(model, message.id) MAX per file reproduced csift's reported totals exactly in all four fields on the test transcript. Only the quoted over-read range needed correction, and it is worth restating as a per-field range since a single number cannot cover both the aggregate and input_tokens alone. code-site note: src/stats.rs:250-256 still carries the comment quoting '2.2-3.5x (measured)'. Measured now over 15 transcripts the four-field aggregate factor spans 1.91-3.17x (median 2.64x) and the input_tokens-only factor reaches 4.73x, so the comment's range is narrow at the bottom and understates the single-field worst case."
}
]
},
{
"id": "QT-041",
"area": "queue-and-telemetry",
"behavior": "A compaction replay can emit a SECOND record for the same `message.id` whose four top-level `message.usage` counts are ZEROED while the original copy carries the real values, so the copies of one id are not interchangeable. Measured over a 15-transcript sample, this is rare but real: 1 of the 3,865 message.ids appearing on more than one record within a file had non-identical usage across its copies, and that group's zeroed copies sit immediately after a `compact_boundary` record and repeat the pre-boundary records' timestamps. The zeroing is PARTIAL: it clears `input_tokens`, `output_tokens`, `cache_read_input_tokens` and `cache_creation_input_tokens` while the nested `cache_creation`, `output_tokens_details` and `iterations` values on the same record keep their real numbers - so 'zeroed usage' means the four fields csift reads, not the whole object.",
"depends": "this is why the `stats` dedupe takes the per-field MAX rather than first-wins: on clean data the two rules agree, but a first-wins rule would let traversal order decide whether an API message contributes its real tokens or zeros.",
"code": [
{
"path": "src/stats.rs",
"lines": "250-256",
"snippet": " // CC repeats the IDENTICAL message.usage on every per-block record of\n // one API message; summing per record over-reports 2.2-3.5x (measured).\n // Dedupe per FILE by message.id, taking the per-field MAX across the\n // id's admitted records: identical on clean data, and immune to the\n // compaction-replay shape where a replayed copy carries ZEROED usage\n // (first-wins would depend on traversal order). An id-less record\n // counts on its own, as before."
},
{
"path": "src/stats.rs",
"lines": "307",
"snippet": " for ((model, _), vals) in usage_peak {"
}
],
"instrument": "Census the `message.id` values appearing on more than one record within ONE file (one observation per (message.id, record) pair), compare those copies' whole `message.usage` objects as canonical JSON, then re-read the lines of any differing group printing record type, subtype, isCompactSummary and the four counts, so the zeroed copies can be located relative to the `compact_boundary` record. Cross-check that `csift stats` on that file reports the non-zero values.",
"located": {
"claude_code": null,
"csift": "0.9.2",
"source": "AGENTS.md section 1; src/stats.rs:250-256 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\nmulti=ident=diff=mixed=0\nfor p in sample:\n g=collections.defaultdict(list)\n for i,raw in enumerate(open(p,'rb'),1):\n if b'\"usage\"' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n if r.get('type')!='assistant': continue\n m=r.get('message') or {}; u=m.get('usage')\n if not isinstance(u,dict) or not m.get('id'): continue\n g[m['id']].append((i,json.dumps(u,sort_keys=True)))\n for mid,rows in g.items():\n if len(rows)<2: continue\n multi+=1; s={x[1] for x in rows}\n if len(s)==1: ident+=1\n else:\n diff+=1\n t=[json.loads(x) for x in s]\n z=[x for x in t if not any((x.get(k) or 0) for k in\n ('input_tokens','output_tokens','cache_read_input_tokens','cache_creation_input_tokens'))]\n if z and len(z)<len(t): mixed+=1; print('differing group at lines', [x[0] for x in rows])\nprint('multi',multi,'identical',ident,'differing',diff,'zero+nonzero',mixed)\nPY AND csift stats <that transcript> --no-subagents --format json AND a targeted Python re-read of the record lines the census named, printing type, subtype, isCompactSummary, timestamp, block types and the four usage counts",
"observed": "Over the 15-transcript sample, 3,865 message.ids appear on more than one record within one file; 3,864 have byte-identical usage across every copy and exactly 1 differs. That one group spans 13 records in a single transcript: 10 records before a `type:\"system\" subtype:\"compact_boundary\"` line carry (input 2, output 4,849, cache_read 951,431, cache_creation 16,024), and 3 records AFTER the boundary carry (0, 0, 0, 0) while reusing the SAME message.id and repeating the pre-boundary timestamps to the second. A second, partial form of the same shape appeared in another transcript: the two per-block copies of one API message immediately following an isCompactSummary record have all four top-level counts zeroed while output_tokens_details.thinking_tokens (2218), cache_creation.ephemeral_1h_input_tokens (2140) and the `iterations` array (input_tokens 2, output_tokens 2,973, cache_read_input_tokens 962,321, cache_creation_input_tokens 2,140) all still carry real values - so the zeroing hits the four top-level scalars, not the whole object. Cross-check: `csift stats` on the first transcript reports the NON-ZERO values (summed across models: input 664, output 530,169, cache_read 93,433,115, cache_creation 16,454,008), matching an independent per-(model, message.id) MAX reimplementation exactly.",
"rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). GROUP = all records in ONE file sharing a message.id; one observation per (message.id, record) pair. A group is DIFFERING when the canonical JSON of its copies' usage objects is not all one string; it is MIXED when at least one copy has all four token counts 0 and at least one does not.",
"note": "Code sites re-checked: src/stats.rs:250-256 and :307 contain the claimed snippets verbatim at the claimed lines. The claim is confirmed by instrument, and the per-field MAX is vindicated for a reason worth recording: in the group measured here the zeroed copies come LAST, so a first-wins rule would have survived by luck, while in the partial-zeroing case the zeroed copies are the ONLY copies of that id - which is why a MAX cannot rescue that one either and the honest statement is that MAX is order-independent, not that it recovers every case."
}
]
},
{
"id": "REC-001",
"area": "record-model",
"behavior": "Claude Code writes exactly one JSON object per LINE into `~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl`, so the record unit is the physical line: a compact line and a space-reserialized line are each one whole record, while a pretty-printed multi-line object breaks the jsonl framing.",
"depends": "csift mmaps the file, splits it on newlines with `memchr` and parses only candidate lines, and every address (`show --line`), every count and the malformed-line census are expressed in lines; a record spread over several lines would be booked as several malformed lines rather than one record.",
"code": [
{
"path": "src/parse/lines.rs",
"lines": "101-109",
"snippet": "pub(crate) fn line_payload(line: &[u8]) -> Option<&[u8]> {\n let line = line.strip_suffix(b\"\\n\").unwrap_or(line);\n let line = line.strip_suffix(b\"\\r\").unwrap_or(line);\n if line.iter().all(u8::is_ascii_whitespace) {\n None\n } else {\n Some(line)\n }\n}"
}
],
"instrument": "`wc -l <transcript>` equals the summed line-type census PLUS `skipped_lines` from the same `csift stats --format json` object, not the census alone: a torn or pretty-printed record contributes lines to skipped_lines and to none of the line_types buckets. On a clean file skipped_lines is 0 and the two figures coincide, which is why the shorter form usually appears to work.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.9",
"source": "dev session 2026-09-02; CHANGELOG 0.6.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) live: `wc -l ~/.claude/projects/<enc>/<session>.jsonl` beside `csift stats @<session> --no-subagents --format json | jq '.line_types, .skipped_lines'`; (b) fixture: a 3-record file written one record compact, one record space-reserialized (Python json.dumps defaults), one record pretty-printed (indent=2), then `csift --claude-home <tmp-home> stats @<session> --format json` and `csift --claude-home <tmp-home> search <NEEDLE> -c` once per record.",
"observed": "Live file: wc -l = 14283; summed line_types = 14283 (assistant 2316, attachment 8116, user 1278, last-prompt 467, permission-mode 465, mode 465, ai-title 462, agent-name 439, queue-operation 120, system 98, file-history-snapshot 57); skipped_lines = 0. Fixture: wc -l = 18 physical lines (1 compact + 1 space-reserialized + 16 lines of one pretty-printed object); census line_types = {\"user\": 2}; skipped_lines = 16; 2 + 16 = 18. Needle in the compact record: 1 exchange. Needle in the space-reserialized record: 1 exchange. Needle in the pretty-printed record: 0.",
"rule": "One record per physical jsonl line, whole file. The identity that must hold is sum(line_types) + skipped_lines == wc -l, counted once per non-blank physical line.",
"note": "Both halves of the framing law were exercised, not just the positive one. The pretty-printed object was booked as 16 malformed lines and zero records, and its text was unreachable by search - exactly the failure mode the claim predicts. The space-reserialized record stayed a full citizen (matched, censused as `user`), confirming the serialization-tolerant candidate matching the claim's depends clause rests on. Code site src/parse/lines.rs 101-109 is verbatim at the claimed lines."
}
]
},
{
"id": "REC-002",
"area": "record-model",
"behavior": "The top-level `type` discriminator is an OPEN set: the harness maps a known type to a persistence class and falls back to a default for anything unrecognised, so an unknown value is written and read without error. All thirteen values the claim names were observed on disk, and three more the claim omits were observed too - `atis-latch`, `bridge-session` and `cost-state` - for sixteen distinct values over the whole local corpus. `type:\"summary\"` is an ON-DISK absence (0 of 1285920 lines), not a harness fact: `summary` IS a registered type in the 2.1.258 routing table (class `last-wins`). A compaction summary on disk is a `type:\"user\"` record flagged `isCompactSummary`.",
"depends": "csift keeps `type` as a tolerant `Option<String>` and branches in logic rather than deserializing an enum, and compaction detection keys on `isCompactSummary` instead of a `summary` type; a closed enum would panic every scanning subcommand on the next type Claude Code ships.",
"code": [
{
"path": "src/model/record.rs",
"lines": "17-20",
"snippet": " /// Record discriminator: \"user\", \"assistant\", \"system\", \"summary\",\n /// \"last-prompt\", \"attachment\", … (open set - keep as String, never enum-panic).\n #[serde(default)]\n pub r#type: Option<String>,"
},
{
"path": "src/search/scan.rs",
"lines": "333-338",
"snippet": "/// §7d stage-1 category prefilter on raw bytes: keep a line only if it could be a\n/// transcript message (user/assistant role marker) - drops `attachment`,\n/// `file-history-*`, `queue-operation`, and metadata noise pre-JSON unless the\n/// matching [`CandidateGates`] flag admits them. Kept deliberately permissive\n/// (substring, not structural) so no genuine turn is lost.\npub(crate) fn line_is_transcript_candidate(line: &[u8], gates: &CandidateGates) -> bool {"
}
],
"instrument": "`csift stats --format json | tail -1` over the whole corpus is the sound form: stats fully validates every line for the census, so a type it does not list has zero records. The `rg -o '\"type\":\"[a-z-]+\"'` alternative also matches the string `\"type\":\"text\"` and every other nested block/payload discriminator, so it over-reports unless anchored to line start.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 3.1; src/model/record.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "`csift stats --format json | tail -1` (whole corpus under ~/.claude/projects, subagent transcripts included - stats fully validates every line for its line-type census) and `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'user:\"transcript\".{0,1400}'`.",
"observed": "Corpus census over 7577 transcripts / 1285920 lines, skipped_lines 0, 16 distinct top-level `type` values: attachment 466495, assistant 428960, user 228125, last-prompt 29233, permission-mode 29174, mode 29085, ai-title 28756, queue-operation 13494, system 12690, agent-name 12309, file-history-snapshot 4055, file-history-delta 1971, atis-latch 797, bridge-session 728, fork-context-ref 33, cost-state 15. `summary` = 0 records. The 2.1.258 binary carries a record-type routing table with 38 keys and an open default: 'user:\"transcript\",assistant:\"transcript\",system:\"transcript\",attachment:\"transcript\",progress:\"boundary-cleared\",\"file-history-snapshot\":\"boundary-cleared\",\"file-history-delta\":\"boundary-cleared\",\"last-prompt\":\"boundary-cleared\",\"continued-in\":\"boundary-cleared\",... ,\"fork-context-ref\":\"accumulate\",\"frame-link\":\"accumulate\",summary:\"last-wins\",\"custom-title\":\"last-wins\",\"ended-by-model\":\"last-wins\",\"ai-title\":\"last-wins\",tag:\"last-wins\",relocated:\"last-wins\",\"agent-name\":\"last-wins\",\"agent-color\":\"last-wins\",\"agent-setting\":\"last-wins\",\"pr-link\":\"last-wins\",...,\"bridge-session\":\"last-wins\",\"history-suppression\":\"last-wins\",\"attribution-snapshot\":\"last-wins\",mode:\"last-wins\",\"permission-mode\":\"last-wins\",\"isolation-latch\":\"last-wins\",\"atis-latch\":\"last-wins\",\"worktree-state\":\"last-wins\",\"cost-state\":\"last-wins\",\"queue-operation\":\"last-wins\",\"observer-ref\":\"last-wins\"};function fts(e){return dts[e]??\"accumulate\"}'",
"rule": "One count per raw jsonl line, whole file, across every transcript under ~/.claude/projects (top-level and subagent). Binary side: one registry entry per key in the routing table; the `?? \"accumulate\"` default is what makes the set open by construction.",
"note": "The binary registry is the stronger instrument for openness than any disk census: it enumerates 38 record types of which only 16 have ever been written here, and 22 of the 38 (progress, continued-in, content-replacement, frame-link, custom-title, ended-by-model, tag, relocated, agent-color, agent-setting, pr-link, history-suppression, attribution-snapshot, isolation-latch, worktree-state, observer-ref, summary and the four-name origami/artifact families) are unobserved locally. Both code sites are verbatim at the claimed lines."
}
]
},
{
"id": "REC-003",
"area": "record-model",
"behavior": "Only the two message-bearing types carry a `message{}` field: re-measured over 413 transcripts in one project directory, assistant 37218/37218 and user 18431/18431 carry one while all 13 other Claude-Code-written line types in scope carry zero.",
"depends": "csift makes `message{}` presence the decisive instrument for `Class::llm_visible()`, so a bare-role selector (`-t user`) returns only leaves that could become an API message; if a non-message type ever gained a `message{}` the visibility split behind `search -t` would silently invert.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "184-190",
"snippet": " pub fn llm_visible(self) -> bool {\n !matches!(\n self,\n Class::UserUnsent\n | Class::UserQueued\n | Class::CompactionBoundary\n | Class::MetaTurnDuration"
},
{
"path": "src/model/record.rs",
"lines": "111-113",
"snippet": " /// The role-bearing message payload (present on user/assistant records).\n #[serde(default)]\n pub message: Option<Message>,"
}
],
"instrument": "Parse every line of a set of transcripts with a strict JSON reader and cross-tabulate top-level `type` against presence of the key `message`; counting rule = one count per LINE, per file. Expect 100% presence on `user`/`assistant` and 0 on every other type. Re-check whenever a new line type appears in the `csift stats --format json` line-type census.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Strict-JSON deep parse of every line of all 413 transcripts under one project directory (`find <project-dir> -name '*.jsonl' ! -name journal.jsonl`), cross-tabulating top-level `type` against presence of the top-level key `message`.",
"observed": "136059 lines, 0 unparseable. With `message`: assistant 37218/37218 (100%), user 18431/18431 (100%). Without `message`, all zero with: attachment 61147, last-prompt 3507, mode 3484, permission-mode 3484, ai-title 3103, agent-name 2651, queue-operation 1083, system 863, file-history-snapshot 433, atis-latch 233, bridge-session 233, file-history-delta 144, fork-context-ref 18. Two further zero-message types in the sweep were csift's own elicitation sidecar markers (csift-elicitation 4, csift-elicitation-resolved 23), not written by Claude Code.",
"rule": "One count per non-blank physical line per file; a line counts under its top-level `type` and under `message` present / absent. 100% presence expected on user and assistant, 0 on every other type.",
"note": "The decisive-instrument property the depends clause rests on is intact: zero non-message line types carry a `message{}`, so `Class::llm_visible` never has to arbitrate an ambiguous line. Both code sites (src/model/taxonomy.rs 175-181, src/model/record.rs 104-106) are verbatim at the claimed lines."
}
]
},
{
"id": "REC-004",
"area": "record-model",
"behavior": "The eleven named fields (type, uuid, parentUuid, timestamp, sessionId, cwd, version, gitBranch, isSidechain, userType, message) are present on 100% of user and assistant records. `subtype` is present on 100% of system records but `content` is NOT: only 13 of 98 system records carried it in the sampled file, so `content` is a per-subtype field, not a system-record field. `isCompactSummary` appeared on 5 user records, matching the file's 5 compactions. The set is wide and evolving: user records carried 26 distinct top-level keys, 15 of them beyond the claim's list (promptId, entrypoint, slug, session_id, toolUseResult, sourceToolAssistantUUID, promptSource, permissionMode, origin, isMeta, isVisibleInTranscriptOnly, isCompactSummary, toolDenialKind, queuePriority, interruptedMessageId).",
"depends": "csift deserializes only what it uses and ignores the rest, so a new harness field never crashes a scan; tightening the model into a strict schema would break every subcommand on the next Claude Code release.",
"code": [
{
"path": "src/model/record.rs",
"lines": "92-99",
"snippet": " /// `system` record inline content (e.g. away_summary text, or the `compact_boundary`\n /// `\"Conversation compacted …\"` line). Read by `search` as the message-less fallback text (D7).\n #[serde(default)]\n pub content: Option<serde_json::Value>,\n\n /// `compact_boundary` metrics (§3.5 / D7): `{trigger, preTokens, postTokens, durationMs}` on a\n /// `type:\"system\"`/`subtype:\"compact_boundary\"` record. Kept RAW; `search` renders it as a\n /// readable excerpt (`record_raw_text`) so `-t harness.compaction.boundary` can enumerate"
}
],
"instrument": "`head -200 ~/.claude/projects/*/<any>.jsonl | jq -r 'keys[]' | sort | uniq -c` - expect the listed keys plus extras; then `csift stats @<session>` on the same file and confirm zero malformed lines. Counting rule: one key occurrence per record.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md section 3.2"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Strict-JSON parse of one 14283-line transcript, tabulating every top-level key by record `type`; then `csift stats @<session> --no-subagents --format json` on the same file for the malformed count.",
"observed": "user records 1278: parentUuid, isSidechain, promptId, type, message, uuid, timestamp, userType, entrypoint, cwd, sessionId, version, gitBranch all 1278/1278; then slug 1261, session_id 1219, toolUseResult 1197, sourceToolAssistantUUID 1197, promptSource 43, permissionMode 41, origin 40, isMeta 8, isVisibleInTranscriptOnly 5, isCompactSummary 5, toolDenialKind 2, queuePriority 2, interruptedMessageId 1 (26 distinct keys). assistant records 2316: parentUuid, isSidechain, message, type, uuid, timestamp, userType, entrypoint, cwd, sessionId, version, gitBranch all 2316/2316; requestId 2315, session_id 2315, slug 2291, attributionSkill 14, isApiErrorMessage 2, error 1, errorDetails 1, apiErrorStatus 1. system records 98: subtype 98/98 but content only 13/98; compactMetadata 5 and logicalParentUuid 5 (the file's 5 compactions). stats reported skipped_lines 0 on the same file.",
"rule": "One key occurrence per record; percentages are over records of that `type` in the one file.",
"note": "The tolerance the depends clause asks for is doing real work: a strict schema built from the claim's eleven fields would have rejected keys on every single record of the sampled file. Code site src/model/record.rs 85-92 is verbatim at the claimed lines."
}
]
},
{
"id": "REC-005",
"area": "record-model",
"behavior": "`timestamp` is an ISO-8601 UTC string with a trailing `Z` (for example `2026-06-07T05:43:00.000Z`) on message-bearing records, but it is ABSENT entirely on many line types - the session-state cache lines (`last-prompt`, `ai-title`, `agent-name`, `mode`, `permission-mode`), `file-history-snapshot`, `fork-context-ref`, `cost-state`, `atis-latch` and `bridge-session` - and those metadata-only records also lack `uuid` and `parentUuid`.",
"depends": "Every csift field is `Option<T>` and no surface unwraps a timestamp: the time windows (`--since`/`--until`), turn ordering and the `list`/`stats` spans are computed only over timestamped records, and `show --uuid`/`--turn` can never address a cache line (only `show --line`/`--raw` reaches it).",
"code": [
{
"path": "src/model/record.rs",
"lines": "28-30",
"snippet": " /// ISO8601 UTC, e.g. `2026-06-07T05:43:00.000Z`. Absent on metadata-only records.\n #[serde(default)]\n pub timestamp: Option<String>,"
},
{
"path": "src/timez.rs",
"lines": "79-81",
"snippet": "pub fn format_timestamp(raw: Option<&str>) -> String {\n render_local(raw, false)\n}"
},
{
"path": "src/stats.rs",
"lines": "240-243",
"snippet": " if let Some(ts) = rec.timestamp.as_deref() {\n if out.first_utc.as_deref().is_none_or(|f| ts < f) {\n out.first_utc = Some(ts.to_string());\n }"
},
{
"path": "src/model/classify_promoted.rs",
"lines": "9-11",
"snippet": " /// The single promoted leaf a NON-message line carries (v0.10.0), or `None` for a\n /// message record and for every line type that stays unmodeled (the session-state\n /// cache lines, the unpromoted system subtypes, a content-less queue `dequeue`)."
}
],
"instrument": "`rg -m5 '\"type\":\"mode\"' ~/.claude/projects/**/*.jsonl | jq -r 'has(\"timestamp\"), has(\"uuid\")'` must print `false false`, and `csift stats` over the same file must still report a span; then `csift show @<id> --line <a cache line number>` must bail with the 'no such record(s)' message while `--raw` prints the bytes. Counting rule: per raw line.",
"located": {
"claude_code": "2.1.258",
"csift": "0.1.0",
"source": "SPEC.md section 3.2; src/model/record.rs comment; AGENTS.md section 3.2"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Strict-JSON parse of all 407 Claude-Code-written transcripts under one project directory, tabulating presence of `timestamp` / `uuid` / `parentUuid` by top-level `type` and regex-checking every timestamp string against ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$; a separate keys dump of a `cost-state` record from another project directory; then `csift show @<session> --line 2` and `csift show @<session> --line 2 --raw` against a line that is a `mode` cache record.",
"observed": "All 118992 timestamp strings matched the trailing-Z ISO-8601 shape; zero other shapes. Zero timestamp, zero uuid and zero parentUuid on: agent-name 2653, ai-title 3105, atis-latch 235, bridge-session 235, file-history-snapshot 433, fork-context-ref 18, last-prompt 3509, mode 3486, permission-mode 3486. cost-state keys were exactly [hasUnknownModelCost, modelUsage, sessionId, startTime, totalAPIDuration, totalAPIDurationWithoutRetries, totalCostUSD, totalDuration, totalLinesAdded, totalLinesRemoved, totalToolDuration, type] - no timestamp, no uuid, no parentUuid. By contrast user 18471, assistant 37265, system 863 and attachment 61166 carried all three at 100%. `csift show --line 2` on the mode line exited 1 with `csift: error: no such record(s): L2 - ... session-state cache lines (last-prompt, mode, ai-title, ...) ... are inspectable with --raw`, while `--raw` exited 0 and printed {\"type\":\"mode\",\"mode\":\"normal\",\"sessionId\":\"...\"}.",
"rule": "One count per non-blank physical line, bucketed by top-level `type`; a field counts as present iff the key exists on the parsed object. Timestamp shape is checked per timestamp string, whole corpus of the sampled directory.",
"note": "Every one of the ten line types the claim names was confirmed timestampless, uuidless and parentUuidless, and the timestamp shape held on all 118992 strings with no exception. Two types NOT in the claim's list behave differently and are worth recording so a future reader does not over-generalise: `queue-operation` (1083/1083) and `file-history-delta` (144/144) DO carry a top-level `timestamp` while still carrying no `uuid` and no `parentUuid` - so 'timestampless' and 'uuidless' are not the same partition of the line types. All four code sites are verbatim at the claimed lines."
}
]
},
{
"id": "REC-006",
"area": "record-model",
"behavior": "Every record carries a top-level `version` stamp naming the Claude Code build that wrote it and a `gitBranch` field, and BOTH can change mid-session: an upgrade or a branch switch lands inside one transcript (one deep sample spanned builds 2.1.150 through 2.1.258).",
"depends": "`list` reports `version`/`git_branch` as LAST-seen with the opening values on `version_first`/`git_branch_first` plus a drift arrow, and `search --count-by version` censuses where an upgrade landed; reporting only the opening sample was stale.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "134-140",
"snippet": " // The base fields are LAST-seen (what the session is on NOW); the head\n // capture becomes the *_first pair. Either window can be empty - fall back\n // to the other so a one-record session reports the same value everywhere.\n version: version_last.clone().or_else(|| version.clone()),\n version_first: version.or(version_last),\n git_branch: git_branch_last.clone().or_else(|| git_branch.clone()),\n git_branch_first: git_branch.or(git_branch_last),"
}
],
"instrument": "`csift search '' @<uuid> --count-by version` (records per stamp, stampless records excluded and disclosed) beside `csift list @<uuid> --format json | jq '{version_first, version_last, git_branch_first, git_branch_last}'`. Counting rule: one record per version key.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.3",
"source": "CHANGELOG 0.8.1 (list last-seen version/branch; --count-by version); dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "`csift search '' @<session> --count-by version --no-subagents` on two sessions, and `csift list --no-subagents --max-count 0 --format json` over every project, comparing version_first/version_last and git_branch_first/git_branch_last per session row.",
"observed": "One session's version census returned three keys in one transcript: 2.1.233 7051 records, 2.1.227 1818, 2.1.258 1811 (10680 matched across 3 version keys); a second session returned a single key (2.1.207, 3597). Across 64 top-level session rows, 11 had version_first != version_last and 2 had git_branch_first != git_branch_last (main -> HEAD in one, HEAD -> main in the other). Distinct build stamps across the head/tail samples: 32, min 2.1.150, max 2.1.258. The text row rendered the drift arrow as `(branch main, CC 2.1.227->2.1.258)`, and the JSON row carried version 2.1.258 / version_first 2.1.227 / version_last 2.1.258 / git_branch main / git_branch_first main / git_branch_last main.",
"rule": "Version census: one record per version key, records with no version stamp excluded and disclosed. Drift census: one session row per top-level transcript; a session counts as drifted iff its head-window value differs from its tail-window value.",
"note": "Both halves are confirmed independently. Version drift is common (11 of 64 sessions, 17%) and one transcript carried three distinct build stamps including a downgrade (2.1.233 before 2.1.227), which a first-sample-only reader would report as a single wrong build. gitBranch drift is rarer (2 of 64) and both instances were a detached-HEAD checkout crossing the session. The corpus range 2.1.150 through 2.1.258 reproduces the claim's figure exactly, though here it spans 32 builds over 64 sessions rather than one deep sample. Code site src/session/summarize.rs 134-140 is verbatim at the claimed lines, and the JSON key names the claim's instrument uses (version_last, git_branch_last) do exist alongside version_first/git_branch_first."
}
]
},
{
"id": "REC-007",
"area": "record-model",
"behavior": "On an `assistant` record the `message` object is the FULL model-API message object. Nine keys are universal - model, id, type, role, content, stop_reason, stop_sequence, stop_details, usage - `diagnostics` is near-universal but NOT guaranteed (99.98%, absent on 7 of 37374 records), and rarer keys appear on a handful of records (context_management, container).",
"depends": "csift models `role` + `content` plus a lazily-parsed `usage` blob; `stats` reads `message.model` and `message.usage` for its per-model token table and the live tail state machine reads `stop_reason`, so tightening `Message` breaks both.",
"code": [
{
"path": "src/model/record.rs",
"lines": "261-263",
"snippet": " /// `Record::tool_use_result`): only `stats` reads it, via [`Message::token_usage`].\n #[serde(default)]\n pub usage: Option<Box<serde_json::value::RawValue>>,"
}
],
"instrument": "`rg -m1 '\"type\":\"assistant\"' <one transcript> | jq '.message | keys'` samples exactly one record, which cannot distinguish a universal key from a 99.98% key. Tabulate keys over every assistant record of a file instead.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 3.3"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Strict-JSON parse of all 408 transcripts under one project directory, tabulating every key of `message{}` on `type:\"assistant\"` records; plus `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\\{[a-z_]+:!0,(?:[a-z_]+:!0,){4,}[a-z_]+:!0\\}'` for the harness's stop_reason registry.",
"observed": "37374 assistant records. Present on 100%: model, id, type, role, content, stop_reason, stop_sequence, stop_details, usage. Present on 37367/37374 (99.98%): diagnostics. Rare: context_management 13 (0.03%), container 12 (0.03%). The 2.1.258 binary carries the stop_reason value registry '{end_turn:!0,max_tokens:!0,stop_sequence:!0,tool_use:!0,pause_turn:!0,compaction:!0,refusal:!0,model_context_window_exceeded:!0}'.",
"rule": "One count per key per assistant record, over every assistant record in the sampled directory; percentage is over the 37374 assistant records.",
"note": "The two consumers the depends clause names are both safe: `message.model` and `message.usage` are universal, so the stats token table never loses a record to an absent key, and `stop_reason` is universal on assistant records too. The stop_reason value space the harness recognises is wider than the five values usually quoted - it includes pause_turn, compaction and model_context_window_exceeded - which matters for any consumer that branches on the value rather than on its inequality to end_turn. Code site src/model/record.rs 254-256 is verbatim at the claimed lines."
}
]
},
{
"id": "REC-008",
"area": "record-model",
"behavior": "A record's `message.content` is polymorphic: either a bare STRING (older format, genuine user text, a compaction summary) or an ARRAY of typed blocks; both shapes occur in current data.",
"depends": "csift models `Content` as an untagged string-or-array enum; a struct that assumed the array form would drop every string-content genuine user turn, which is the majority shape for human prose.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "53-58",
"snippet": " pub fn content_str(&self) -> Option<&str> {\n match self.content.as_ref()? {\n serde_json::Value::String(s) => Some(s.as_str()),\n _ => None,\n }\n }"
},
{
"path": "src/model/predicates.rs",
"lines": "68-73",
"snippet": " match &msg.content {\n Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n Some(Content::Blocks(blocks)) => {\n let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n if !has_text || has_tool_result {"
}
],
"instrument": "`rg -m20 '\"type\":\"user\"' <transcript> | jq -r '.message.content | type' | sort | uniq -c` must show both `string` and `array`. Counting rule: one per sampled user record.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 3.4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(a) per-project-directory byte census with `rg -c --no-filename -I -F '\\\"role\\\":\\\"user\\\",\\\"content\\\":[' <dir> -g '*.jsonl'` against the same needle ending in a quote, summed over all 14 project directories; (b) `csift search '' -t user.message --raw --max-count 0` piped into a strict-JSON reader that types `message.content` per record.",
"observed": "Corpus byte census: 212800 user lines with array-form content, 15568 with string-form content - both shapes present, neither rare. Among the records csift labels as genuine human turns (`-t user.message`, whole corpus): 2421 records, of which 1946 string (80.4%) and 475 array (19.6%). A separate strict parse of one project directory gave user array 17452 / user string 1077 with the same conclusion.",
"rule": "Byte census: one count per raw line matching the anchored needle, summed per project directory. Label census: one count per record returned by `-t user.message`, typed on the parsed `message.content` value.",
"note": "The depends clause's stronger sub-claim was checked and holds: the bare-string shape is the MAJORITY shape for human prose, 1946 of 2421 genuine user turns (80.4%). An array-only reader would therefore drop four fifths of the human turns, not an edge case. Both code sites (src/model/classify_promoted.rs 48-53, src/model/predicates.rs 68-73) are verbatim at the claimed lines."
}
]
},
{
"id": "REC-009",
"area": "record-model",
"behavior": "Assistant `message.content` is ALWAYS a block array in Claude Code 2.1.x; a bare string body would be a genuine surprise.",
"depends": "`agent_text` still handles the bare-string case rather than dropping it, so a format change surfaces as content instead of a silent blank in `verbatim` and `search -t agent.message`.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "357-360",
"snippet": " let Content::Blocks(blocks) = content else {\n // Assistant content is always a block array in CC 2.1.x; a bare string\n // would be a genuine surprise - surface it rather than silently drop.\n if let Content::Text(s) = content {"
}
],
"instrument": "`rg -c '\\\"type\\\":\\\"assistant\\\".*\\\"content\\\":\\\"'` is not sound as written - the `.*` lets any later `\\\"content\\\":\\\"` on the same line satisfy it, including one nested inside a tool_use input - so a nonzero result would not prove a string body. It returned 0 on each of the three largest transcripts tested, agreeing with the deep parse, but the anchored form `\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"` plus a deep parse of the residual lines is the form that actually decides it.",
"located": {
"claude_code": "2.1.x",
"csift": null,
"source": "src/model/exchange.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Two passes. (a) per-project-directory byte census over all 14 project directories: `rg -c -F '\\\"type\\\":\\\"assistant\\\"'`, `rg -c -F '\\\"role\\\":\\\"assistant\\\",\\\"content\\\":['` and the same needle ending in a quote. (b) a strict-JSON deep parse of every line of all 7579 transcripts that carries the assistant tag but NOT the compact array needle, typing `message.content` on each, so the byte census's key-order blind spot is closed rather than assumed away.",
"observed": "Corpus: 429501 assistant lines; 428539 matched the compact array needle; 0 matched the string needle. The 828 residual lines (those carrying the assistant tag and lacking the compact needle) all deep-parsed to `content = array` - a different key order, not a different shape - and none was a bare string. A separate full strict parse of one project directory independently gave 37374 assistant records with 0 bare-string content.",
"rule": "One count per raw line for the byte pass; one count per parsed assistant record for the deep pass. An assistant record counts as string-shaped iff `message.content` deserialises to a JSON string.",
"note": "Zero bare-string assistant bodies in 429501 assistant records across 7579 transcripts and 32 distinct build stamps (2.1.150 through 2.1.258). The claim is as strong as an on-disk negative can be here. The tolerant arm in src/model/exchange.rs 357-360 is verbatim at the claimed lines and is currently dead on this corpus, which is exactly its purpose - it exists so the surprise surfaces as content rather than as a blank."
}
]
},
{
"id": "REC-010",
"area": "record-model",
"behavior": "Content blocks are internally tagged on `type` and the set is open and considerably wider than the six named. Observed on disk in one project directory: tool_use (id, name, input, and a `caller` field valued exactly {\"type\":\"direct\"} on 17363 of 17363), tool_result (tool_use_id, content, and is_error on only 68.9%), thinking (thinking plus a signature that was present on 11674 of 11674 blocks here, so optional in the schema but universal in practice), text, image (source), and `fallback` - a model-fallback marker {\"type\":\"fallback\",\"from\":{\"model\":...},\"to\":{\"model\":...}} the claim omits entirely. `redacted_thinking` is registered by the harness and carries an opaque `data` payload but was never written on this machine (0 blocks corpus-wide). The 2.1.258 binary registers 22 block types, so 16 of them are unobserved here.",
"depends": "csift's `Block` enum carries a `#[serde(other)] Unknown` arm so an unmodeled future block type parses instead of failing the whole line; losing it turns one new block type into a malformed-line count across every command.",
"code": [
{
"path": "src/model/record.rs",
"lines": "340",
"snippet": " Thinking {"
},
{
"path": "src/model/record.rs",
"lines": "362-367",
"snippet": " ToolResult {\n #[serde(default, rename = \"tool_use_id\")]\n tool_use_id: Option<String>,\n /// String OR array of {type:text,text}/{type:image} - keep raw.\n #[serde(default)]\n content: Option<serde_json::Value>,"
},
{
"path": "src/model/narration.rs",
"lines": "51-54",
"snippet": "pub(crate) fn thinking_block_class(signature: Option<&str>) -> Class {\n let Some(sig) = signature else {\n return Class::AgentThinking;\n };"
}
],
"instrument": "`rg -o '\"type\":\"(text|thinking|redacted_thinking|tool_use|tool_result|image)\"' <transcript> | sort | uniq -c` enumerates the live set, and `csift search '' -t agent --count-by label` must account for each. Counting rule: one per block occurrence in raw bytes for the rg, one per record for the census.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "SPEC.md section 3.5; AGENTS.md section 3.2; src/model/record.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) strict-JSON parse of all 408 transcripts under one project directory, censusing every content block by its `type` and every key within each block type; (b) `csift search 'redacted thinking' -t agent.thinking --raw` over the whole corpus, piped to `grep -c '\\\"type\\\":\\\"redacted_thinking\\\"'` to separate real blocks from prose mentions; (c) `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\\{[a-z_]+:!0,(?:[a-z_]+:!0,){4,}[a-z_]+:!0\\}'`.",
"observed": "Block census: tool_use 17363 (type/id/name/input/caller all 100%), tool_result 17352 (tool_use_id 100%, content 100%, is_error 68.9%), thinking 11674 (type/thinking/signature all 100%), text 8509 (type/text 100%), image 46 (type/source 100%), and one block of a type the claim does not list: fallback 1, shaped {\"type\":\"fallback\",\"from\":{\"model\":\"<model-id>\"},\"to\":{\"model\":\"<model-id>\"}}. Every one of the 17363 tool_use blocks carried caller exactly {\"type\": \"direct\"}. Corpus-wide, 0 real `redacted_thinking` blocks exist (the 37 records matching the phrase under -t agent.thinking were prose in development sessions; grep over their raw lines found 0 occurrences of the block type). The 2.1.258 binary registers 22 block types: '{advisor_tool_result:!0,bash_code_execution_tool_result:!0,code_execution_tool_result:!0,compaction:!0,container_upload:!0,document:!0,fallback:!0,image:!0,mcp_tool_result:!0,mcp_tool_use:!0,mid_conv_system:!0,redacted_thinking:!0,search_result:!0,server_tool_use:!0,text:!0,text_editor_code_execution_tool_result:!0,thinking:!0,tool_result:!0,tool_search_tool_result:!0,tool_use:!0,web_fetch_tool_result:!0,web_search_tool_result:!0}'. The binary also confirms the redacted payload field: 'if(e.type===\"redacted_thinking\")return Wc(e.data,n)'.",
"rule": "One count per block occurrence inside a parsed `message.content` array, per file; key percentages are over blocks of that type. Binary side: one entry per key of the block-type registry.",
"note": "The `#[serde(other)] Unknown` arm the depends clause names is at src/model/record.rs 369 and is load-bearing right now, not hypothetically: the single `fallback` block on disk is already an unmodeled type, and 16 further registered types (document, search_result, server_tool_use, mcp_tool_use, mcp_tool_result, container_upload, compaction, mid_conv_system and the five *_tool_result execution families) would each become a malformed-line count without it. All three code sites (src/model/record.rs 333, src/model/record.rs 355-360, src/model/narration.rs 51-54) are verbatim at the claimed lines."
}
]
},
{
"id": "REC-011",
"area": "record-model",
"behavior": "A `tool_result` block's `content` is either a plain STRING or an ARRAY containing `{type:\"text\",text}`, `{type:\"image\",...}` and `{type:\"tool_reference\", tool_name}` entries - the last emitted by ToolSearch results.",
"depends": "csift keeps `tool_result.content` as a raw `Value` and inspects it on demand; a `String`-typed field would drop every array-form tool result, which is where deferred-tool schemas and image results live.",
"code": [
{
"path": "src/model/record.rs",
"lines": "362-367",
"snippet": " ToolResult {\n #[serde(default, rename = \"tool_use_id\")]\n tool_use_id: Option<String>,\n /// String OR array of {type:text,text}/{type:image} - keep raw.\n #[serde(default)]\n content: Option<serde_json::Value>,"
}
],
"instrument": "`rg -c '\"type\":\"tool_reference\"' ~/.claude/projects/**/*.jsonl` in a session that used ToolSearch, then `csift show @<id> --line <that line>` must render without error; `jq -r '.message.content[]? | select(.type==\"tool_result\") | (.content|type)' <file> | sort | uniq -c` gives the string-vs-array split. Counting rule: one per raw occurrence.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 3.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, collections, glob\nshape = collections.Counter(); inner = collections.Counter()\nfiles = [f for f in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in f]\nfor f in files:\n for line in open(f, errors='replace'):\n line = line.strip()\n if not line: continue\n try: r = json.loads(line)\n except Exception: continue\n m = r.get('message')\n if not isinstance(m, dict): continue\n ct = m.get('content')\n if not isinstance(ct, list): continue\n for b in ct:\n if isinstance(b, dict) and b.get('type') == 'tool_result':\n cc = b.get('content')\n shape[type(cc).__name__] += 1\n if isinstance(cc, list):\n for e in cc:\n if isinstance(e, dict): inner[str(e.get('type'))] += 1\nprint(dict(shape), dict(inner))\nPY\n# run over two separate project dirs; plus:\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'tool_reference' | head -3",
"observed": "project dir A (414 transcripts): content shapes str=16938, list=348; array element types text=257, tool_reference=137. project dir B (7 transcripts): str=11877, list=392; element types text=251, tool_reference=120, image=71. A verbatim array element: {\"type\": \"tool_reference\", \"tool_name\": \"EnterPlanMode\"}. Binary 2.1.258 contains the literal string `tool_reference`. Code site src/model/record.rs:355-360 present verbatim, including the doc line `/// String OR array of {type:text,text}/{type:image} - keep raw.`",
"rule": "One count per tool_result block, reached as record -> message.content[] -> block.type=='tool_result', keyed by the JSON type of that block's own `content` (str vs list). Array element counts are one per element, keyed by element `type`. Whole project dir, every *.jsonl including subagent transcripts, journal.jsonl excluded. Two project dirs scanned separately.",
"note": "All three element shapes the claim names are present: text and tool_reference in both dirs, image in the second. tool_reference carries exactly the `tool_name` key the claim states. The string form dominates (16938:348 and 11877:392), so a String-typed field would have dropped ~2% of tool results including every deferred-tool schema and every image result."
}
]
},
{
"id": "REC-012",
"area": "record-model",
"behavior": "A thinking block can arrive as `redacted_thinking`: Claude Code writes `\"type\":\"redacted_thinking\"` with no readable `thinking` text, only an opaque `data` payload.",
"depends": "csift classifies it `agent.thinking` and emits the constant placeholder `[redacted thinking]` as the hit text; the placeholder is synthesized but stays prefilter-sound only because both of its words occur verbatim in the raw bytes of `\"type\":\"redacted_thinking\"` and any bracketed or whitespaced pattern is prefilter-ineligible anyway.",
"code": [
{
"path": "src/model/record.rs",
"lines": "346-348",
"snippet": " /// A `redacted_thinking` block - encrypted/opaque reasoning CC emits in place of a visible\n /// `thinking` block (no readable text, only an opaque `data` payload). Classified\n /// `agent.thinking` exactly like a normal thinking block"
},
{
"path": "src/search/types.rs",
"lines": "17",
"snippet": "pub(crate) const REDACTED_THINKING_PLACEHOLDER"
},
{
"path": "src/model/taxonomy.rs",
"lines": "64-65",
"snippet": " /// `agent.thinking` - a thinking block (see the GOLD-gap note re `redacted_thinking`).\n AgentThinking,"
}
],
"instrument": "`rg -cNI '\"redacted_thinking\"' ~/.claude/projects -g '*.jsonl'` then `csift search 'redacted' @<id> -t agent.thinking`; counting rule = per record. A zero corpus count means the block type is absent locally, not that the arm is wrong - it is exercised by a synthetic fixture and the e2e taxonomy tests pin the placeholder render.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "src/model/record.rs comment; AGENTS.md section 3.3a (redacted_thinking mapping); dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'redacted_thinking'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\"redacted_thinking\"[^\"]{0,40}' | sort -u | head -20\ncd ~/.claude/projects && for d in */; do n=$(rg -cNI --no-filename '\"redacted_thinking\"' -g '*.jsonl' \"$d\" | awk '{s+=$1} END{print s+0}'); echo \"$n $d\"; done",
"observed": "Binary 2.1.258 carries the block schema `\"redacted_thinking\"),data:i()})` (a `data` field and no `thinking` field), the reader `\"redacted_thinking\":return d.inputTextCharLength+=f.data?.l`, the bare 17-byte string `redacted_thinking`, and the guard regex '`?(thinking|redacted_thinking)`?\\s+(or\\s+`?redacted_thinking`?\\s+)?blocks?\\s+.{0,60}cannot be modified'. Corpus: 0 matching lines in each of 14 project dirs (14 separate scans, all zero). Code sites present verbatim: src/model/record.rs:339 doc line, src/model/record.rs:343 `RedactedThinking {`, src/search/types.rs:17 `pub(crate) const REDACTED_THINKING_PLACEHOLDER: &str = \"[redacted thinking]\";`, src/model/taxonomy.rs:64-65 (`AgentThinking,` under the redacted_thinking doc note).",
"rule": "Binary: one count per distinct `strings -n 6` line containing the token. Corpus: one count per raw jsonl line containing the quoted token `\"redacted_thinking\"`, summed within a project dir, each of the 14 project dirs scanned separately.",
"note": "The block type is live in CC 2.1.258 and the binary settles the payload shape the claim asserts: the serialized block is `{type:\"redacted_thinking\", data: <opaque>}` with no `thinking` text, and a separate reader measures its length off `.data`. Zero occurrences in this machine's corpus is the claim's own stated expectation, not a refutation; the csift arm is exercised by fixture instead. The prefilter-soundness sub-argument checks out by inspection: the raw bytes `\"type\":\"redacted_thinking\"` contain both the substrings `redacted` and `thinking`, and the bracketed, space-bearing placeholder is prefilter-ineligible under csift's own whitespace rule."
}
]
},
{
"id": "REC-013",
"area": "record-model",
"behavior": "`tool_result` blocks live inside a `type:\"user\"` CARRIER record's `message.content` array and never on an assistant record (46,916/46,916 measured). Every block carries its own `tool_use_id`; the carrier also repeats the join structurally as a top-level `toolUseResult` and `sourceToolAssistantUUID`, but no measured carrier depends on those to be joinable.",
"depends": "csift joins `tool_result.tool_use_id == tool_use.id` for the `agent.tool.use` / `agent.tool.result` pairing marker and the `pairing` JSON enum. csift implements NO fallback to `toolUseResult` or `sourceToolAssistantUUID`; a block-level `tool_use_id` is the only join key, and a result whose use is out of scope renders as `orphan`.",
"code": [
{
"path": "src/model/record.rs",
"lines": "122-124",
"snippet": " /// tree on demand via [`Record::tool_use_result_value`].\n #[serde(default, rename = \"toolUseResult\")]\n pub tool_use_result: Option<Box<serde_json::value::RawValue>>,"
}
],
"instrument": "`csift search '' -t agent.tool.result --count-by pairing --format json` - the `orphan` bucket counts results whose use is out of scope or unjoinable. Counting rule: one per matched record, each counted once on the pairing axis.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 4.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, collections, glob\nby = collections.Counter(); noid = 0; tot = 0\nfor f in [x for x in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in x]:\n for line in open(f, errors='replace'):\n line = line.strip()\n if not line: continue\n try: r = json.loads(line)\n except Exception: continue\n m = r.get('message')\n if not isinstance(m, dict): continue\n ct = m.get('content')\n if not isinstance(ct, list): continue\n for b in ct:\n if isinstance(b, dict) and b.get('type') == 'tool_result':\n tot += 1; by[(r.get('type'), m.get('role'))] += 1\n if not b.get('tool_use_id'): noid += 1\nprint(tot, dict(by), 'missing tool_use_id:', noid)\nPY\ncsift search '' @<session> -t agent.tool.result --count-by pairing\ncd <csift repo> && rg -n 'sourceToolAssistantUUID|source_tool_assistant' src/",
"observed": "46,916 tool_result blocks across 6 project dirs (5 dirs scanned whole, plus the 4 oldest sessions of a sixth). 46,916/46,916 sit on records with top-level type \"user\" AND message.role \"user\"; 0 on any assistant record. 0/46,916 blocks lacked a block-level `tool_use_id` - including the 4 oldest sessions, whose `version` stamps span 2.1.170 to 2.1.187. `sourceToolAssistantUUID` IS present as a carrier field (17,353 lines in one project dir). csift pairing census on one session: 6944 paired, 0 orphan, 0 pending. `rg -n 'sourceToolAssistantUUID|source_tool_assistant' src/` returns NO matches - csift implements no such fallback. Code site src/model/record.rs:115-117 present verbatim.",
"rule": "One count per tool_result block (record -> message.content[] -> block.type=='tool_result'), keyed by the pair (record top-level `type`, message `role`); a block is 'missing tool_use_id' when the block-level key is absent or falsey. Whole project dirs, journal.jsonl excluded. The pairing census counts each matched record once on the pairing axis.",
"note": "The carrier-placement half of the claim holds with a strong count. The 'some legacy carriers omit the block-level tool_use_id' half is refuted for every build this corpus reaches (0 of 46,916, back to CC 2.1.170) and, more decisively, the fallback the claim's `depends` credits csift with does not exist in the source. What would decide the legacy half: a transcript written by a Claude Code build older than 2.1.170, which this machine has none of."
}
]
},
{
"id": "REC-014",
"area": "record-model",
"behavior": "`toolUseResult` is a structured echo on tool-result carriers that routinely embeds the whole file body or stdout a tool returned (plus structured patches and persisted-output metadata). Measured over four transcripts it is 7-18% of ALL candidate-line bytes (0.074, 0.111, 0.171, 0.178) and 54-60% of the bytes of the carrier lines that actually have one (0.538, 0.545, 0.575, 0.598).",
"depends": "csift keeps `toolUseResult` and `attachment` UNPARSED as `Box<RawValue>` and reads the small fields the hot paths need through a cheap typed probe, building a `Value` tree only for the deep consumers (`files`, `recover`, `image`); reverting either to an eager `Value` regresses the performance contract on 200MB+ transcripts.",
"code": [
{
"path": "src/model/record.rs",
"lines": "115-119",
"snippet": " /// Structured echo on tool-result carriers (§4.6). Kept as UNPARSED raw JSON text\n /// (`Box<RawValue>`) rather than a built `Value` tree: this blob routinely carries\n /// the full file/output content a tool returned (≈20-25% of a candidate line's\n /// bytes), and eagerly tree-building it for EVERY carrier dominated the parse cost\n /// of every scanning subcommand."
}
],
"instrument": "Sum the `toolUseResult` byte length over a transcript's candidate lines and divide by total candidate bytes (jq over `csift search '' @<id> --raw`); counting rule = bytes of the `toolUseResult` value versus bytes of the whole line, over candidate (role-bearing) lines only. Then benchmark `csift search <literal> <project-dir>` with hyperfine before and after any change to the raw-field handling.",
"located": {
"claude_code": null,
"csift": "0.6.0",
"source": "SPEC.md section 7c; src/model/record.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && python3 - <<'PY'\nimport json\ndec = json.JSONDecoder()\nfor f in [<four transcripts, one per project dir>]:\n cand_bytes = tur_bytes = carr_bytes = cand = carr = 0\n for line in open(f, errors='replace'):\n s = line.rstrip('\\n')\n if not s: continue\n role = ('\"role\":\"user\"' in s) or ('\"role\":\"assistant\"' in s)\n if role: cand += 1; cand_bytes += len(s.encode())\n i = s.find('\"toolUseResult\":')\n if i < 0: continue\n carr += 1; carr_bytes += len(s.encode())\n start = i + len('\"toolUseResult\":')\n _, end = dec.raw_decode(s, start)\n tur_bytes += len(s[start:end].encode())\n print(f, cand, cand_bytes, carr, carr_bytes, tur_bytes)\nPY",
"observed": "Four transcripts, one per project dir. Share of CANDIDATE (role-bearing) line bytes: 0.1776 (cand=10,640 lines / 53,999,530 B; toolUseResult=9,588,461 B), 0.1107 (30,774 / 290,092,392 B; 32,119,165 B), 0.1712 (18,505 / 58,279,708 B; 9,977,525 B), 0.0741 (1,849 / 35,379,227 B; 2,621,115 B). Share of CARRIER-line bytes only (lines that actually carry a toolUseResult): 0.5745, 0.5378, 0.5445, 0.5980. Carriers per file: 3,361 / 7,563 / 5,903 / 551. Code sites present verbatim: src/model/record.rs:108-112 (the Box<RawValue> rationale doc, which itself states the disputed \"~20-25% of a candidate line's bytes\") and src/model/predicates.rs:358-360 (the tur_probe doc).",
"rule": "The toolUseResult VALUE span is measured exactly, not estimated: locate the byte offset of the literal key `\"toolUseResult\":` in the raw line, then `json.JSONDecoder().raw_decode` from just past it to get the value's end offset; the span is UTF-8 byte length. Denominator A = summed UTF-8 byte length of every candidate line, where candidate = csift's stage-1 keep (the raw line contains a user or assistant role marker). Denominator B = summed byte length of only those lines that carry a toolUseResult. Whole file, one file per project dir.",
"note": "The design rationale is unaffected and confirmed - the blob is genuinely huge (54-60% of the bytes of any line that carries one) and keeping it as Box<RawValue> is what avoids paying for it. Only the headline percentage is wrong: 20-25% matches neither natural counting rule. The claimed figure sits between the two measured denominators, which is consistent with it having been produced by a third, unrecorded rule; the correction states both rules explicitly so the number is reproducible."
}
]
},
{
"id": "REC-015",
"area": "record-model",
"behavior": "When a tool output is too large Claude Code replaces the inline `tool_result.content` with a `<persisted-output>` block reading `Output too large (<human size>). Full output saved to: <ABSOLUTE_PATH>` plus `Preview (first <human size>):` - both sizes are rendered by a byte-formatting function, observed as `(81.1KB)` and `(2KB)`, not a literal `NNN KB`. It writes the real bytes to the session sidecar `<ENCODED>/<session-uuid>/tool-results/<id>.txt`, where `<id>` is either a short tool id or, for hook output, `hook-<tool_use_id>-<n>-additionalContext` (the API `toolu_...` tool_use id, not a session uuid) - 166 of 1,863 sidecar files corpus-wide take the hook form. It ALSO records the location structurally as `toolUseResult.persistedOutputPath` with `persistedOutputSize` in bytes alongside (100/100 paired in the measured project dir).",
"depends": "`search --resolve-persisted` prefers the structured path (exact, no regex) and falls back to scraping the inline `Full output saved to:` marker, substituting the file content BEFORE matching; a read failure is non-fatal and appends an explicit note. Without the resolution the searchable text is only the 2 KB preview.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "313-320",
"snippet": " /// The persisted-output file path for this carrier (§4.6), preferring the\n /// structured `toolUseResult.persistedOutputPath` (exact - no regex) and falling\n /// back to scraping the inline `Full output saved to: <path>` marker from a\n /// `tool_result` block. Returns `None` when there is no persisted pointer.\n #[must_use]\n pub fn persisted_output_path(&self) -> Option<String> {\n // Structured field first (SPEC §4.6 resolution rule).\n if let Some(probe) = self.tur_probe() {"
},
{
"path": "src/model/grouping.rs",
"lines": "249-256",
"snippet": "/// Scrape the inline persisted-output pointer (§4.6 fallback): the line\n/// `Full output saved to: <ABSOLUTE_PATH>` inside a `<persisted-output>` block.\n/// Returns the trimmed path, or `None` if the marker is absent.\npub(crate) fn scrape_persisted_path(text: &str) -> Option<String> {\n const MARKER: &str = \"Full output saved to:\";\n let idx = text.find(MARKER)?;\n let rest = &text[idx + MARKER.len()..];\n // The path runs to end-of-line."
},
{
"path": "src/model/record.rs",
"lines": "306-307",
"snippet": " #[serde(rename = \"persistedOutputPath\")]\n pub(crate) persisted_output_path: Option<serde_json::Value>,"
}
],
"instrument": "`rg -c 'persistedOutputPath' ~/.claude/projects/**/*.jsonl` and `ls ~/.claude/projects/<enc>/<uuid>/tool-results/ | head`, then `csift search '<a needle only in the persisted file>' @<id> --resolve-persisted` must match while the same search without the flag must not. Counting rule: matched records with and without the flag; one pointer per externalised tool result.",
"located": {
"claude_code": "2.1.258",
"csift": "0.1.0",
"source": "SPEC.md section 4.6; AGENTS.md section 3.6; src/model/exchange.rs resolve doc; SPEC.md section 4.6 as consumed by section 6.2; src/model/exchange.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, glob, collections\nkeys = collections.Counter(); forms = collections.Counter()\nfor f in [x for x in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in x]:\n for line in open(f, errors='replace'):\n if 'persistedOutput' not in line: continue\n t = (json.loads(line).get('toolUseResult') or {})\n if not isinstance(t, dict) or not t.get('persistedOutputPath'): continue\n for k in t:\n if 'persisted' in k.lower(): keys[k] += 1\n base = t['persistedOutputPath'].rsplit('/', 1)[-1]\n forms['hook-form' if base.startswith('hook-') else 'short-id-form'] += 1\nprint(dict(keys), dict(forms))\nPY\ncd ~/.claude/projects && find . -path '*/tool-results/*' -name '*.txt' | sed 's|.*/||' > /tmp/n.txt; wc -l < /tmp/n.txt; rg -c '^hook-' /tmp/n.txt\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Output too large \\(\\$\\{[^}]*\\}[^\"`]{0,60}|Preview \\(first[^\"`]{0,40}'\ncsift search '<a token present only in the persisted file>' @<session> # -> matched 1 exchange\ncsift search '<same token>' @<session> --resolve-persisted # -> matched 3 exchanges",
"observed": "1,863 tool-results/*.txt files corpus-wide; 166 of them named `hook-toolu_<id>-<n>-additionalContext.txt`, the rest short tool ids like `b0425lu0i.txt`. In one project dir, 100/100 records carrying `persistedOutputPath` also carry `persistedOutputSize` (a byte count, e.g. 83070); the path shape is ~/.claude/projects/<ENCODED>/<session-uuid>/tool-results/<id>.txt. A verbatim inline marker: `<persisted-output>\\nOutput too large (81.1KB). Full output saved to: <ABS_PATH>\\n\\nPreview (first 2KB):`. Binary 2.1.258 templates: `Output too large (${Ut(n.originalSize)}). Full output saved to: ${n.filepath}` and `Preview (first ${Ut(pNe)}):`. Round trip: the same search returned 'matched 1 exchange' bare and 'matched 3 exchanges' with --resolve-persisted. Code sites present verbatim: src/model/exchange.rs:313-320, src/model/grouping.rs:249-256, src/model/record.rs:299-300.",
"rule": "One pointer per externalised tool result: a record whose `toolUseResult` object has a non-empty `persistedOutputPath`; the field pairing is counted per such record. Sidecar file counts are one per *.txt under any `tool-results/` directory, corpus-wide, bucketed by whether the basename starts with `hook-`. The round trip counts matched exchanges for one literal token chosen because it occurs in the persisted file beyond byte 200,000 and nowhere in the transcript itself.",
"note": "Every structural assertion holds; two surface details needed correcting. The `<id>` hook form embeds an API tool_use id, not a uuid, and the marker's size is a formatted human size rather than the `NNN KB` shape the claim spelled out. The --resolve-persisted round trip is the strongest part: the needle exists only past byte 200,000 of the sidecar file, well beyond the 2 KB preview, and only the flagged run finds it."
}
]
},
{
"id": "REC-016",
"area": "record-model",
"behavior": "`attachment` is a TOP-LEVEL sibling of `message`, not a content block, and each payload carries its own `type`. 28 distinct payload types were observed in a single project dir, including every value the claim names except the auto_mode pair: hook_success, hook_additional_context, edited_text_file, `file` snapshots, compact_file_reference, plan_mode (and plan_mode_exit), queued_command, task_reminder, date_change, deferred_tools_delta and skill_listing, plus total_tokens_reminder, batching_reminder_sent, bash_output_audience_note, hook_cancelled, ultrathink_effort, silent_turn_reminder, remote_session_change, agent_listing_delta, command_permissions, read_truncation_notice, plan_file_reference, hook_system_message, workflow_keyword_request, invoked_skills, hook_blocking_error and task_status. The steering configs `auto_mode` / `auto_mode_exit` / `auto_mode_scan` are defined in Claude Code 2.1.258 but absent from this corpus; `auto_mode` carries `{autoModeConsentFlow, bashFirst, steerOnly, bypass}` and `auto_mode_exit` carries only `{bashFirst, steerOnly}`.",
"depends": "`search --attachments` (and `--count-by attachment`, which implies the gate) makes them searchable with the verbatim payload JSON as matchable text under `harness.meta.attachment`, and `plan`'s binding read and `recover`'s external-edit reader parse the top-level field directly; steering PROSE is often not persisted while its config attachment is, so a zero-match for a steering keyword proves nothing without the gate.",
"code": [
{
"path": "src/model/record.rs",
"lines": "126-129",
"snippet": " /// Top-level `attachment` payload (a sibling of `message`, not a content block).\n /// Real records carry attachments for hook output, `edited_text_file` external\n /// edits, `file` snapshots, etc. Kept as UNPARSED raw JSON text (same rationale as\n /// `tool_use_result` - attachments embed whole file snapshots)"
},
{
"path": "src/model/predicates.rs",
"lines": "306-310",
"snippet": " pub fn attachment_type(&self) -> Option<String> {\n if !self.is_type(\"attachment\") {\n return None;\n }\n let v = self.attachment_value()?;"
}
],
"instrument": "`csift search '' @<session> --count-by attachment` lists every payload `type` with its record count; counting rule = one count per `type:\"attachment\"` record, keyed by `attachment.type`.",
"located": {
"claude_code": null,
"csift": "0.8.1",
"source": "src/model/record.rs comment; CHANGELOG 0.8.1; SKILL.md wrong-assumption row on rollout/steering keywords"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, glob, collections\natt = collections.Counter(); sibling = collections.Counter()\nfor f in [x for x in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in x]:\n for line in open(f, errors='replace'):\n line = line.strip()\n if not line: continue\n try: r = json.loads(line)\n except Exception: continue\n if 'attachment' not in r: continue\n a = r['attachment']\n att[a.get('type') if isinstance(a, dict) else type(a).__name__] += 1\n sibling['has_message=' + str('message' in r)] += 1\n sibling['top_level_type_is_attachment=' + str(r.get('type') == 'attachment')] += 1\nprint(len(att), att.most_common(), dict(sibling))\nPY\ncsift search '' @<session> --count-by attachment\ncd ~/.claude/projects && for d in <4 project dirs>; do rg -cNI --no-filename '\"type\":\"auto_mode' -g '*.jsonl' \"$d\" | awk '{s+=$1} END{print s+0}'; done\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'type:\"auto_mode[^,}]{0,10}[^\"]{0,80}' | sort -u\ncsift search 'total_tokens_reminder' @<session> -c # -> 20\ncsift search 'total_tokens_reminder' @<session> --attachments -t harness.meta.attachment -c # -> 148",
"observed": "61,181 records carrying a top-level `attachment` in one project dir; 61,181/61,181 have top-level `type:\"attachment\"` and 61,181/61,181 have NO `message` key, confirming attachment is a top-level sibling rather than a content block. 28 distinct payload `type` values in that dir: hook_success 45448, hook_additional_context 9909, total_tokens_reminder 2627, task_reminder 638, skill_listing 545, batching_reminder_sent 488, deferred_tools_delta 460, queued_command 237, bash_output_audience_note 224, edited_text_file 112, hook_cancelled 70, ultrathink_effort 62, silent_turn_reminder 58, remote_session_change 57, file 45, agent_listing_delta 41, compact_file_reference 39, date_change 34, command_permissions 25, read_truncation_notice 22, plan_file_reference 15, plan_mode 6, plan_mode_exit 6, hook_system_message 4, workflow_keyword_request 4, invoked_skills 3, hook_blocking_error 1, task_status 1. `auto_mode` / `auto_mode_exit`: 0 lines in each of 4 project dirs, but present in binary 2.1.258 as `type:\"auto_mode\",autoModeConsentFlow:!o&&GKe(n),bashFirst:_,steerOnly:d,bypass:o}` and `type:\"auto_mode_exit\",bashFirst:r.bashFirst,steerOnly:r.steerOnly}`, alongside a third value `type:\"auto_mode_scan\"`. Gate round trip on one literal: 20 matches bare, 148 with `--attachments -t harness.meta.attachment`. Code sites present verbatim: src/model/record.rs:119-122, src/model/predicates.rs:306-310.",
"rule": "One count per raw jsonl line carrying a top-level `attachment` key, keyed by `attachment.type`; whole project dir, subagent transcripts included, journal.jsonl excluded. The sibling check counts the same lines by whether they also carry a top-level `message` key. The gate round trip counts matched records (`-c`) for one literal with and without the attachment gate, same session, same pattern.",
"note": "The structural core is confirmed exactly. Two corrections: the auto_mode payload shape the claim gives is incomplete (it omits `autoModeConsentFlow`, and `auto_mode_exit` has no `bypass`), and the observed payload set is materially wider than the claim's list - 28 types in one project dir, so the enumeration should read as open. The `--attachments` searchability the `depends` asserts is measured working: the same literal goes from 20 to 148 matched records once the gate admits attachment lines."
}
]
},
{
"id": "REC-017",
"area": "record-model",
"behavior": "`attachment` is the DOMINANT record type - 54.3% of all lines in the largest sampled file (49,902 of 91,975) and 57.7% in the next one sampled - across 29 payload subtypes in that largest file. The bulk is `hook_success` (76.6% of its attachment records), which is hook output for EVERY hook event, dominated by PostToolUse (31,973) and PreToolUse (10,608); SessionStart is a small minority (231 of 45,448 in the project dir censused).",
"depends": "csift's stage-1 prefilter drops attachment lines pre-JSON unless a gate admits them (`--additional-context`, `--attachments`, `--count-by attachment`, or an explicit `show` address); admitting them by default doubles scan cost and floods every search.",
"code": [
{
"path": "src/search/scan.rs",
"lines": "333-338",
"snippet": "/// §7d stage-1 category prefilter on raw bytes: keep a line only if it could be a\n/// transcript message (user/assistant role marker) - drops `attachment`,\n/// `file-history-*`, `queue-operation`, and metadata noise pre-JSON unless the\n/// matching [`CandidateGates`] flag admits them. Kept deliberately permissive\n/// (substring, not structural) so no genuine turn is lost.\npub(crate) fn line_is_transcript_candidate(line: &[u8], gates: &CandidateGates) -> bool {"
},
{
"path": "src/search/scan.rs",
"lines": "356-357",
"snippet": " static ATTACHMENT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memmem::Finder::new(b\"\\\"attachment\\\"\"));"
}
],
"instrument": "`csift stats @<id> --format json | jq '.line_types'` gives the whole-file census, and `rg -o '\"attachment\":\\{\"type\":\"[a-z_]+\"' <transcript> | sort | uniq -c | sort -rn` enumerates subtypes. Counting rule: attachment lines over all lines in one file for the 54% figure, one count per raw line.",
"located": {
"claude_code": null,
"csift": "0.8.1",
"source": "SPEC.md section 4.8"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && python3 - <<'PY'\nimport json, collections\nfor f in [<the two largest transcripts>]:\n types = collections.Counter(); att = collections.Counter(); tot = 0\n for line in open(f, errors='replace'):\n line = line.strip()\n if not line: continue\n tot += 1\n try: r = json.loads(line)\n except Exception: types['__malformed__'] += 1; continue\n types[str(r.get('type'))] += 1\n a = r.get('attachment')\n if isinstance(a, dict): att[a.get('type')] += 1\n print(tot, types['attachment'], 100*types['attachment']/tot, len(att), att.most_common(8))\nPY\ncsift stats @<session> --format json --no-subagents | jq '.line_types'",
"observed": "Largest transcript: 91,975 lines, 49,902 with `type:\"attachment\"` = 54.3%, 29 distinct payload subtypes; within its attachments hook_success 38,225 (76.6%), hook_additional_context 7,559 (15.1%), total_tokens_reminder 2,591 (5.2%), task_reminder 657 (1.3%). Second-largest sampled transcript: 47,602 lines, 27,454 attachment = 57.7%, 24 subtypes. `csift stats --format json` line_types on that second file: attachment 27454 of lines 47602, skipped_lines 0. hookEvent census over one project dir's 45,448 hook_success records: PostToolUse 31,973, PreToolUse 10,608, Stop 2,087, SubagentStart 335, SessionStart 231, PostToolUseFailure 113, SubagentStop 100, UserPromptSubmit 1. Code sites present verbatim: src/search/scan.rs:322-327 (line_is_transcript_candidate doc) and src/search/scan.rs:345-346 (ATTACHMENT_FINDER).",
"rule": "One count per raw jsonl line, whole file, keyed by top-level `type`; the percentage is attachment lines over all non-blank lines of that one file. Subtype count = distinct `attachment.type` values in the same file. The hookEvent split counts one per hook_success attachment record, keyed by `attachment.hookEvent`, over a whole project dir.",
"note": "The headline 54% is exact for the largest file. Two corrections: the subtype count is 29 in that file, not roughly 21, and the claim's attribution of the hook_success bulk to SessionStart output is wrong by two orders of magnitude - PostToolUse supplies 70% of hook_success records while SessionStart supplies 0.5%. The prefilter rationale is unaffected and independently confirmed by the REC-016 and REC-018 gate round trips, where the same literal's match count jumps once attachment lines are admitted."
}
]
},
{
"id": "REC-018",
"area": "record-model",
"behavior": "Exactly ONE `attachment` payload shape carries hook-injected conversation context: `{\"type\":\"hook_additional_context\",\"content\":[...]}` - the text a SessionStart / UserPromptSubmit / other hook injected - where `content` is a string ARRAY in real data (one element per injected block; a bare string is tolerated), carried alongside `hookName` / `hookEvent`, and the record itself has `uuid`, `timestamp` and `parentUuid`.",
"depends": "csift joins the array with a newline in `Record::hook_additional_context_text`, classifies the record `harness.meta.hook`, and scans it only under `search --additional-context` (or the `--attachments` superset) with the candidate needle `&&`-gated so a default scan never parses attachment lines; `show --line`/`--uuid` renders it flag-free under the refetch law. A bare-string-only reader returns nothing on real data.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "329-335",
"snippet": "\n /// Hook-injected `additionalContext` text: a `type:\"attachment\"` record whose payload is\n /// `{\"type\":\"hook_additional_context\",\"content\":[…],…}` - the context a SessionStart /\n /// UserPromptSubmit / … hook injected into the turn. `content` is a string ARRAY in real\n /// data (one element per injected block; joined with `\\n`), tolerated as a bare string.\n /// `None` for every other record shape. Cheap for non-attachment records (one type\n /// compare before any parse).\n #[must_use]"
},
{
"path": "src/model/predicates.rs",
"lines": "336-343",
"snippet": " pub fn hook_additional_context_text(&self) -> Option<String> {\n if !self.is_type(\"attachment\") {\n return None;\n }\n let v = self.attachment_value()?;\n let att = v.as_object()?;\n if att.get(\"type\").and_then(serde_json::Value::as_str) != Some(\"hook_additional_context\") {\n return None;"
},
{
"path": "src/model/predicates.rs",
"lines": "348-352",
"snippet": " (!s.is_empty()).then(|| s.to_string())\n }\n serde_json::Value::Array(parts) => {\n let texts: Vec<&str> = parts.iter().filter_map(serde_json::Value::as_str).collect();\n (!texts.is_empty()).then(|| texts.join(\"\\n\"))"
},
{
"path": "src/search/scan.rs",
"lines": "350-351",
"snippet": " static HOOK_CONTEXT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memmem::Finder::new(b\"hook_additional_context\"));"
},
{
"path": "src/cli/search_args.rs",
"lines": "493",
"snippet": " /// Also scan hook-injected `additionalContext`: the `attachment` records a SessionStart /"
}
],
"instrument": "`jq -r 'select(.type==\"attachment\") | .attachment.type' <transcript> | sort | uniq -c` must show `hook_additional_context` and `jq '.attachment.content | type'` on one such line must print `array`; then `csift search '<a string your hook injects>' @<session> --additional-context` must match while the same search without the flag must not. Counting rule: one attachment record per hook injection.",
"located": {
"claude_code": "2.1.237",
"csift": "0.7.6",
"source": "AGENTS.md section 3.2; CHANGELOG 0.7.6; src/model/predicates.rs hook_additional_context_text doc; SPEC.md section 4.8; SPEC.md section 6 v0.7.6 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, glob, collections\nctype = collections.Counter(); keys = collections.Counter()\ntop = collections.Counter(); ev = collections.Counter(); alen = collections.Counter(); n = 0\nfor f in [x for x in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in x]:\n for line in open(f, errors='replace'):\n if 'hook_additional_context' not in line: continue\n try: r = json.loads(line)\n except Exception: continue\n a = r.get('attachment')\n if not isinstance(a, dict) or a.get('type') != 'hook_additional_context': continue\n n += 1; c = a.get('content'); ctype[type(c).__name__] += 1\n if isinstance(c, list):\n alen[len(c)] += 1\n for e in c: ctype['elem:' + type(e).__name__] += 1\n for k in a: keys[k] += 1\n for k in ('uuid', 'timestamp', 'parentUuid'): top[k + '=' + str(k in r)] += 1\n ev[str(a.get('hookEvent'))] += 1\nprint(n, dict(ctype), dict(alen.most_common(6)), dict(keys), dict(top), dict(ev))\nPY\ncsift search 'Background operation detected' @<session> -c # -> 3\ncsift search 'Background operation detected' @<session> --additional-context -c # -> 94",
"observed": "9,909 hook_additional_context records in one project dir. `content` JSON type: list 9,909, str 0 - a string ARRAY in 100% of real records. 9,983 array elements, all of them strings; array length 1 in 9,890 records, then 5 (8), 6 (7), 2 (2), 4 (1), 3 (1). Payload keys, each present on all 9,909: `type`, `content`, `hookName`, `toolUseID`, `hookEvent`. Record top level: uuid present 9,909/9,909, timestamp 9,909/9,909, parentUuid 9,909/9,909. hookEvent values: PreToolUse 5,865, PostToolUse 3,180, SubagentStart 405, UserPromptSubmit 254, PostToolUseFailure 113, SubagentStop 48, SessionStart 44. Gate round trip on one injected literal: 3 matched records bare (its own echoes in shell-command records), 94 with `--additional-context`. Code sites present verbatim: src/model/predicates.rs:328-335 and 336-343 and 348-352, src/search/scan.rs:339-340 (HOOK_CONTEXT_FINDER, needle b\"hook_additional_context\"), src/cli/search_args.rs:488.",
"rule": "One count per `type:\"attachment\"` record whose `attachment.type` is `hook_additional_context`, whole project dir including subagent transcripts, journal.jsonl excluded. `content` is keyed by its Python JSON type; element counts are one per array element. Presence counts are per record. The gate round trip counts matched records (`-c`) for one literal that a PostToolUse hook injects, same session and pattern, with and without --additional-context.",
"note": "Every assertion measured true and the array-vs-string point is decisive: 9,909 of 9,909 use the array form and zero use a bare string, so a bare-string-only reader really would return nothing on real data. Two additions worth folding in later rather than corrections: the payload also carries `toolUseID` beside `hookName`/`hookEvent`, and the hook events that inject context are dominated by PreToolUse (5,865) and PostToolUse (3,180) rather than the SessionStart/UserPromptSubmit pair the claim leads with - the claim's 'other hook' wording already covers them."
}
]
},
{
"id": "REC-019",
"area": "record-model",
"behavior": "Assistant messages carry `stop_reason` with the values `tool_use` / `end_turn` / `stop_sequence` / `max_tokens` / `refusal` / null, persisted per record; it is trustworthy on the MAIN lane (measured 0.0-0.3% null) but is NORMALLY null mid-message on subagent lanes, which flush per content block.",
"depends": "csift's `status`/`wait` verdicts classify a child lane `generating` only when the last assistant record's `stop_reason` is not `end_turn`; treating a null on a subagent lane as a finished turn would mark a mid-generation child settled.",
"code": [
{
"path": "src/model/record.rs",
"lines": "247-252",
"snippet": " /// `stop_reason` on assistant messages (`tool_use` / `end_turn` / `stop_sequence` /\n /// `max_tokens` / `refusal` / null). Persisted per record; trustworthy on the MAIN\n /// lane (measured 0.0-0.3% null), NORMALLY null mid-message on subagent lanes (which\n /// flush per content block). Read by the live-truth surfaces. Additive + tolerant.\n #[serde(default, rename = \"stop_reason\")]\n pub stop_reason: Option<String>,"
},
{
"path": "src/live/tail.rs",
"lines": "24-26",
"snippet": " /// The newest assistant record's `stop_reason` (trustworthy on the MAIN lane;\n /// null is NORMAL mid-message on subagents).\n pub(crate) last_stop_reason: Option<String>,"
}
],
"instrument": "`rg -o '\"stop_reason\":(\"[a-z_]+\"|null)' <main transcript> | sort | uniq -c` versus the same over a subagent transcript of the same session. Counting rule: assistant records only, one per line; report null assistant records over all assistant records, per lane.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.0",
"source": "SPEC.md section 6 v0.9.0 ledger; src/model/record.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, glob, collections\ndef census(paths, label):\n c = collections.Counter(); n = 0\n for f in paths:\n for line in open(f, errors='replace'):\n if '\"assistant\"' not in line: continue\n try: r = json.loads(line)\n except Exception: continue\n if r.get('type') != 'assistant': continue\n m = r.get('message')\n if not isinstance(m, dict): continue\n n += 1\n c['null' if m.get('stop_reason') is None else str(m['stop_reason'])] += 1\n print(label, n, c['null'], 100*c['null']/n, dict(c.most_common()))\ncensus([<main transcript>], 'MAIN')\ncensus([s for s in glob.glob('<session>/subagents/**/*.jsonl', recursive=True)\n if not s.endswith('journal.jsonl')], 'SUBAGENTS')\nPY\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'stop_sequence|end_turn|max_tokens|\"refusal\"' | sort | uniq -c\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'stop_reason:[a-zA-Z_.?]{0,30}' | sort -u",
"observed": "Session 1 MAIN lane: 7,036 assistant records, 0 null = 0.00%; values tool_use 6,773, end_turn 257, stop_sequence 6. Its 85 subagent transcripts: 6,890 assistant records, 5,023 null = 72.90%; tool_use 1,845, end_turn 22. Session 2 MAIN lane: 2,316 assistant records, 0 null = 0.00%; tool_use 2,240, end_turn 74, stop_sequence 2. Its 5 subagent transcripts: 493 assistant records, 298 null = 60.45%; tool_use 126, end_turn 69. Binary 2.1.258 string counts: `max_tokens` 110, `\"refusal\"` 80, `stop_sequence` 24, `end_turn` 16, and the literal `stop_reason:null`. Code sites present verbatim: src/model/record.rs:240-245 and src/live/tail.rs:24-26.",
"rule": "Assistant records only - top-level `type == \"assistant\"` with a `message` object - one count per line, keyed by `message.stop_reason` with a missing/None value bucketed as `null`. Null rate = null assistant records over all assistant records, computed separately per lane: the main transcript alone, versus every subagent transcript under that session's `subagents/` tree with journal.jsonl excluded. Two sessions measured independently.",
"note": "The lane split is stark and reproduces on both sessions: the main lane is 0.00% null on 9,352 combined assistant records, while subagent lanes are 60-73% null. That is stronger than the claim's stated 0.0-0.3% main-lane band, so no correction is needed - the band already contains the measurement. Values `max_tokens` and `refusal` were not observed on disk here but are present in the 2.1.258 binary, so the enumeration is the model's stop-reason union rather than a per-corpus observation; a session that hits a token ceiling or a refusal would be what confirms those two on disk."
}
]
},
{
"id": "REC-020",
"area": "record-model",
"behavior": "One API assistant message spans MULTIPLE jsonl records - Claude Code writes one line per content block and repeats the IDENTICAL `message` envelope, `message.usage` included, on every one of them (2,183 of 2,183 multi-record ids measured byte-identical) - so `message.id` (`msg_...`) is the only field joining them; summing usage per record over-reports token totals by a factor measured between 1.66 and 3.48 across 24 (session, model, field) cells, with a typical id spanning 2-3 records and a maximum of 11.",
"depends": "`csift stats` dedupes usage PER FILE by `message.id`, taking the per-field MAX across that id's admitted records; a naive per-record sum makes every token figure csift reports wrong, and that is exactly what pre-0.9.2 csift printed.",
"code": [
{
"path": "src/model/record.rs",
"lines": "254-258",
"snippet": " /// The API message id (`msg_...`). One API message spans MULTIPLE records (CC\n /// writes one line per content block and repeats the message envelope on each),\n /// so this is the usage-dedupe key for `stats`. Additive + tolerant.\n #[serde(default)]\n pub id: Option<String>,"
},
{
"path": "src/stats.rs",
"lines": "249-256",
"snippet": " if let Some(u) = msg.token_usage() {\n // CC repeats the IDENTICAL message.usage on every per-block record of\n // one API message; summing per record over-reports 2.2-3.5x (measured).\n // Dedupe per FILE by message.id, taking the per-field MAX across the\n // id's admitted records: identical on clean data, and immune to the\n // compaction-replay shape where a replayed copy carries ZEROED usage\n // (first-wins would depend on traversal order). An id-less record\n // counts on its own, as before."
}
],
"instrument": "For one transcript, sum `message.usage.{input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens}` per RECORD and again per distinct `message.id`, then divide; counting rule = per session, per model, per field. Measured ratios ranged 2.15-3.48. `csift stats @<id> --format json | jq .tokens` must match the per-msgid figure.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.2",
"source": "SPEC.md section 3.3; SPEC.md section 6 v0.9.2 ledger item 2; AGENTS.md section 1; src/stats.rs comment; CHANGELOG 0.9.2 (stats token sums corrected); CHANGELOG 0.9.2; dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "cd ~/.claude/projects && python3 - <<'PY'\nimport json, collections\nFIELDS = ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens']\nfor f in [<three transcripts>]:\n per = collections.Counter(); byid = {}\n for line in open(f, errors='replace'):\n if '\"usage\"' not in line: continue\n try: r = json.loads(line)\n except Exception: continue\n m = r.get('message')\n if not isinstance(m, dict): continue\n u = m.get('usage')\n if not isinstance(u, dict): continue\n mdl = m.get('model') or '(unknown)'; mid = m.get('id')\n for fld in FIELDS:\n v = u.get(fld) or 0\n per[(mdl, fld)] += v\n k = (mdl, mid, fld)\n byid[k] = v if mid is None else max(byid.get(k, 0), v)\n ded = collections.Counter()\n for (mdl, mid, fld), v in byid.items(): ded[(mdl, fld)] += v\n for k in per: print(f, k, per[k], ded[k], per[k]/ded[k])\nPY\n# identity + join check, same shape, grouping message.usage by message.id\ncsift stats @<session> --format json --no-subagents | jq .tokens",
"observed": "24 (session x model x field) cells over three transcripts. Ratio of the per-RECORD sum to the per-message.id sum ranged 1.663 to 3.481; the extremes were a cache_creation_input_tokens cell at 1.663 and another cache_creation_input_tokens cell at 3.481. The output_tokens cell the claim cites as 3.114 measures 3.119 here. Envelope identity on one session: 3,145 distinct message.id, 2,183 of them spanning >1 record, and 2,183/2,183 of those have byte-identical `message.usage` objects across all their records; 0 ids span more than one `message.model` and 0 span more than one record `type`. Records-per-id histogram: 3 -> 1,242 ids, 1 -> 962, 2 -> 765, 4 -> 110, 5 -> 42, 6 -> 12, 7 -> 5, 8 -> 4; max 11 records for one id. Cross-check: `csift stats --format json | jq .tokens` on that session returned exactly the per-message.id figures for all four fields of both models (e.g. cache_read 1,482,850,339 and output 3,138,067 for one model; cache_read 298,624,093 and output 690,829 for the other), never the per-record sums. Code sites present verbatim: src/model/record.rs:247-251 and src/stats.rs:249-256.",
"rule": "Restrict to records with a `message.usage` object. Numerator = sum of the field over every such RECORD. Denominator = sum over distinct `message.id` of the per-field MAX across that id's records (an id-less record counts on its own). The ratio is computed per cell, one cell per (session, model, field); the 24 cells are 3 sessions x 2 models each x 4 usage fields. Identity check: group usage objects by message.id and compare their canonical JSON.",
"note": "The mechanism, the join key and the dedupe rule all confirm exactly, and the cross-check is the strong part: csift's own stats output equals the per-message.id figure to the token for all eight model-field pairs, so the shipped dedupe is doing what the claim says. Only the ratio band needed widening - the measured floor is 1.66, below the claimed 2.15, and the claim's cited 3.114 output-token reading measures 3.119 on the current file (that session is still being appended to, which accounts for the drift). The claimed ceiling 3.48 reproduces to three decimals."
}
]
},
{
"id": "REC-021",
"area": "record-model",
"behavior": "`role:\"user\"` records NEVER carry `message.usage` (measured 0 across three large sessions); every usage-bearing record is `type:\"assistant\"`.",
"depends": "csift `stats` reads usage only off `rec.message`, so a tool_result carrier contributes nothing to any token total.",
"code": [
{
"path": "src/stats.rs",
"lines": "248-249",
"snippet": " if let Some(msg) = rec.message.as_ref() {\n if let Some(u) = msg.token_usage() {"
}
],
"instrument": "Filter transcript lines to those with a `message.usage` object and tabulate top-level `type`; counting rule = per line. Expect `assistant` = 100%.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.2",
"source": "dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 probe walking every *.jsonl under ~/.claude/projects/<project-dir> (top-level transcripts plus their subagents/, journal.jsonl excluded), json.loads per line, tabulating top-level `type` and `message.role` for every line whose `message.usage` deserializes to an object ; grep -n on src/stats.rs",
"observed": "415 transcripts scanned. 37,379 usage-bearing lines: top-level type = {'assistant': 37379}, message.role = {'assistant': 37379} - zero under any other type or role. The same scope holds 18,531 lines with message.role == 'user', none of which carries a usage object. Restricted to the 23 top-level transcripts alone: 16,713 usage-bearing lines, all assistant. src/stats.rs:248 is ` if let Some(msg) = rec.message.as_ref() {` and 249 ` if let Some(u) = msg.token_usage() {`.",
"rule": "One count per raw jsonl line. A line is usage-bearing iff its top-level `message` deserializes to an object carrying a `usage` object. Percentages are lines, not turns.",
"note": "Refutation attempt failed in the strongest available direction: the scope deliberately included the 392 subagent transcripts, where tool_result carriers are densest, and still produced 0 user-role usage records out of 18,531 user-role lines. Code site verified verbatim at the stated lines."
}
]
},
{
"id": "REC-022",
"area": "record-model",
"behavior": "Because Claude Code streams per-block writes, timestamps DIFFER on essentially every multi-record `message.id` (5,190 of 5,191 measured, median spread 2.6 s, p90 16 s, max 224 s), while `message.model` and the record `type` never differ within one id (0 mismatches measured).",
"depends": "csift's `--since`/`--until`/`--turn` windows can admit some records of an API message and not others; the usage dedupe runs over the ADMITTED set, so max-of-admitted still yields the message's usage once.",
"code": [
{
"path": "src/stats.rs",
"lines": "257",
"snippet": " let model = msg.model_id().unwrap_or(\"(unknown)\").to_string();"
}
],
"instrument": "Group records by `message.id` and compute max(timestamp) - min(timestamp) per group; counting rule = per id. Expect a nonzero spread on most multi-record ids and zero model/type disagreement within an id.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.2",
"source": "dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 probe over the 23 top-level transcripts of ~/.claude/projects/<project-dir>: group every record carrying a non-empty `message.id` by that id, then per group compute max(timestamp)-min(timestamp) and the cardinality of the `message.model` and top-level `type` sets ; grep -n on src/stats.rs",
"observed": "6,816 distinct message.id groups, of which 5,191 span more than one record. 5,190 of those 5,191 (99.98%) show a nonzero timestamp spread. Spread over multi-record groups: median 2.572 s, p90 15.981 s, max 224.243 s. Groups whose `message.model` set has more than one member: 0. Groups whose top-level `type` set has more than one member: 0. src/stats.rs:257 is ` let model = msg.model_id().unwrap_or(\"(unknown)\").to_string();`.",
"rule": "One group per distinct `message.id` within one file. Spread = max minus min of the group's ISO timestamps in seconds. A group counts as a mismatch iff the set of its non-null `message.model` (resp. top-level `type`) values has cardinality > 1.",
"note": "The mechanism and the zero-mismatch half reproduce exactly. The number needed correcting: the claim's '~50 s' upper bound is far too low - the measured maximum in this 23-transcript scope is 224.243 s, and the p90 is 16 s. The consequence for csift is unchanged and, if anything, sharper: a four-minute-wide API message is easy for a narrow --since/--until window to bisect."
}
]
},
{
"id": "REC-023",
"area": "record-model",
"behavior": "Claude Code writes a fabricated stand-in assistant record - an API-error notice, for example - whose `message.model` is the literal placeholder `<synthetic>`, and that model key always reports zero tokens.",
"depends": "`search --count-by model` reports the raw `message.model` value verbatim rather than dropping or normalising it, so a scope's model census discloses the fabricated records instead of hiding them.",
"code": [
{
"path": "src/cli/search_args.rs",
"lines": "422-424",
"snippet": " /// pending under any selector) · `model` (per assistant model, the raw `message.model`\n /// value; Claude Code's own `<synthetic>` placeholder, a CC-fabricated stand-in\n /// assistant record such as an API-error notice, is reported verbatim) · `attachment`"
}
],
"instrument": "`csift search '' <project> --count-by model | rg synthetic` - a `<synthetic>` key must appear verbatim on a corpus that has hit an API error. Counting rule: one record per model key, records with no model excluded and disclosed.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.1",
"source": "SPEC.md section 6 v0.6.1 ledger item 3; CHANGELOG 0.6.1; src/cli/search_args.rs --count-by help; dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search '' <project-dir> --count-by model ; rg --no-filename '\"model\":\"<synthetic>\"' *.jsonl piped to a python3 probe tabulating type, the usage object and the text blocks ; csift stats <project-dir> --format json --max-count 0 summed per model ; strings -n 2 ~/.local/share/claude/versions/2.1.258 | rg -o '.{140}<synthetic>.{60}'",
"observed": "The model census over one project dir prints 7 keys and `<synthetic>` appears verbatim among them with 12 records (largest key 18,434). All 12 are top-level type 'assistant'. Every one carries a usage object whose input_tokens, output_tokens, cache_read_input_tokens and cache_creation_input_tokens are all 0 (two field layouts, 7 + 5). csift stats over the same scope reports `<synthetic> {'cache_creation': 0, 'cache_read': 0, 'input': 0, 'output': 0}` while every real model key is nonzero. Their texts are harness stand-ins, e.g. `API Error: Can't reach the API server - check your internet or DNS (ENOTFOUND)`, `API Error: Server error mid-response. The response above may be incomplete.` and `No response requested.`. The 2.1.258 binary carries the constant table `up=\"(no content)\",TR=\"No response requested.\",Jc=\"<synthetic>\"`.",
"rule": "One record per model key; a record with no `message.model` is excluded from the axis and the excluded count is disclosed (18,551 here). A model key reports zero tokens iff all four summed usage fields are 0.",
"note": "Three independent instruments agree: the census key, the raw per-record usage tabulation, and the binary's own constant. src/cli/search_args.rs:417-419 verified verbatim; the help text's own example ('an API-error notice') is exactly what the 12 records turned out to be."
}
]
},
{
"id": "REC-024",
"area": "record-model",
"behavior": "The session-state cache line types - `last-prompt`, `mode`, `ai-title`, `agent-name`, `permission-mode` - carry no record `uuid`, no `timestamp` and no `message` (they carry only `sessionId`, their one payload field, and on last-prompt a `leafUuid`). The `lastPrompt` value is not a byte copy of a user record: the harness collapses newlines to spaces and truncates at 200 characters plus an ellipsis, and it captures whatever prompt was last DELIVERED - including peer-session inbox messages, which are not genuine user records.",
"depends": "csift deliberately leaves them unpromoted, so they contribute no searchable leaf and no duplicate human turn; promoting them would double-count the last prompt in every `user.message` census.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "9-11",
"snippet": " /// The single promoted leaf a NON-message line carries (v0.10.0), or `None` for a\n /// message record and for every line type that stays unmodeled (the session-state\n /// cache lines, the unpromoted system subtypes, a content-less queue `dequeue`)."
}
],
"instrument": "`jq -r 'select(.type==\"last-prompt\") | keys | join(\",\")' <file> | sort | uniq -c` shows the absent uuid/timestamp/message; then `csift search '<the last prompt text>' @<session> --count-by label` must count it once. Counting rule: one line per cache write, excluded from every leaf.",
"located": {
"claude_code": "2.1.237",
"csift": "0.10.0",
"source": "AGENTS.md section 3.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 probe over the 23 top-level transcripts of ~/.claude/projects/<project-dir> tabulating the sorted top-level key set of every line whose type is last-prompt / mode / ai-title / agent-name / permission-mode ; a second probe comparing each distinct `lastPrompt` value against the text of every user record in one transcript, raw and whitespace-normalized ; rg -c of a distinctive prompt phrase in that transcript vs `csift search '<phrase>' @<session> --count-by label` ; strings -n 2 ~/.local/share/claude/versions/2.1.258 | grep -A3 -F 'normalizeLastPrompt(e){let n=e.replaceAll('",
"observed": "Key sets, one per line type: last-prompt = {lastPrompt, leafUuid, sessionId, type} (3,486 lines) or {leafUuid, sessionId, type} (24 lines); mode = {mode, sessionId, type} (3,487); ai-title = {aiTitle, sessionId, type} (3,106); agent-name = {agentName, sessionId, type} (2,654); permission-mode = {permissionMode, sessionId, type} (3,487). No `uuid`, no `timestamp`, no `message` on any of the 16,244 lines. In one transcript, 312 last-prompt lines carry only 58 distinct values; 45 of the 58 are exactly 201 characters long. Only 9 of 58 appear verbatim inside a user record's text, and 11 of 58 after whitespace normalization. The binary's writer is `normalizeLastPrompt(e){let n=e.replaceAll(`\\n`,\" \").trim();return n.length>200?le(n,200).trim()+\"\\u2026\":n}`. Unpromoted behavior: a distinctive prompt phrase occurs on 6 raw lines of that transcript (2 user, 3 last-prompt, 1 attachment) and `csift search ... --count-by label` reports 2 matched records (user.message 1, harness.compaction.summary 1).",
"rule": "One count per raw jsonl line for the key-set census. lastPrompt duplication is judged per DISTINCT value: a value counts as a duplicate iff some user record's text equals it or starts with it (raw, then again after collapsing whitespace runs to single spaces). The search comparison counts raw LINES against census RECORDS.",
"note": "The structural half - no uuid, no timestamp, no message - reproduces exactly across 16,244 lines and is the half csift's non-promotion rests on; the search comparison shows the 3 cache copies of a phrase contributing nothing to the census. Two wording corrections: the lines do carry `sessionId` (and last-prompt a `leafUuid`), so 'no uuid' means no RECORD uuid; and 'duplicates a record already present as a genuine user message' is too strong - the value is a normalized 201-character-capped rendering, and for peer deliveries its twin classifies agent.communication.inbox rather than user.message. The anti-double-count rationale survives both corrections. Code site verified verbatim at src/model/classify_promoted.rs:9-11."
}
]
},
{
"id": "REC-025",
"area": "record-model",
"behavior": "Claude Code writes a `type:\"file-history-delta\"` line - a sibling of `file-history-snapshot` carrying one path's version bump. Its fields are `{messageId, snapshotMessageId, trackingPath, backup:{backupFileName, version, backupTime}, timestamp}`; unlike the snapshot it DOES carry a top-level timestamp. 1,971 such lines corpus-wide.",
"depends": "`Record::promoted_class` maps both `file-history-snapshot` and `file-history-delta` to `harness.meta.snapshot`; omitting the delta arm loses roughly 1,971 corpus lines from the SEARCHABLE surface. It does not affect recover's backup instrument or files' ExternalWrite inference, which key on `file-history-snapshot` records and the on-disk store.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "27",
"snippet": "\"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),"
}
],
"instrument": "`csift search '' -t harness.meta.snapshot --count-by label --format json` over a corpus session, or `rg -c '\"type\":\"file-history-delta\"' ~/.claude/projects/**/*.jsonl`. Counting rule: one per raw jsonl line.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "SPEC.md section 5.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift stats --format json --max-count 0 over ~/.claude/projects, summing line_types across rows with kind == 'session' only ; rg --no-filename '\"type\":\"file-history-delta\"' *.jsonl in one project dir piped to a python3 key-set and backup-field tabulation ; csift search '' -t harness.meta.snapshot <project-dir> --count-by label ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'insertFileHistoryDelta.{0,300}' ; sed -n on src/model/classify_promoted.rs",
"observed": "Corpus-wide over 7,579 sessions: file-history-delta = 1,971 lines, file-history-snapshot = 4,055. In one project dir, 144 delta lines, all with the identical key set {backup, messageId, snapshotMessageId, timestamp, trackingPath, type}; backup.version was 1 on all 144 and backup.backupFileName was non-null on 79 of 144. `-t harness.meta.snapshot` reaches 577 records in that dir and renders e.g. `[file-history delta at 2026-08-12T01:14:54.653Z: src/path.rs@v1 backup=<hash>@v1]`. The binary writes `insertFileHistoryDelta({type:\"file-history-delta\",messageId:e,snapshotMessageId:n,trackingPath:r,backup:o,timestamp:new Date().toISOString()},d)`. src/model/classify_promoted.rs:13-25 holds the quoted match, with the `\"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),` arm at line 22.",
"rule": "One count per raw jsonl line. Corpus totals sum `line_types` over rows with kind == 'session' ONLY - the stats stream also emits one kind == 'summary' row that re-totals the whole scope, and summing every row double-counts every line type exactly 2x.",
"note": "The line type exists and the mapping is real, so the claim holds in substance. Three corrections. (1) The count is 1,971, not 1,934 - and the counting rule has to travel with it, because summing every stats row instead of only the session rows yields exactly 3,942. (2) `@vN` is csift's own render of `backup.version`, not a Claude Code field name; the CC fields are `trackingPath` and `backup.{backupFileName, version, backupTime}`. (3) The delta feeds only the searchable surface: a grep for tracking_path / backup across src/ reaches src/model/record.rs (the field) and src/search/record_text.rs (the excerpt) and nothing else, while src/files/external.rs and src/recover/snapshots.rs key on `file-history-snapshot`."
}
]
},
{
"id": "REC-026",
"area": "record-model",
"behavior": "A `file-history-snapshot` record nests its timestamp INSIDE the `snapshot` object rather than at top level, so the line has no top-level `timestamp`.",
"depends": "csift's `harness.meta.snapshot` render carries a null hit timestamp and puts the nested instant into the excerpt (`[file-history snapshot at <snapshot.timestamp>: <path>@vN, ...]`); reading a top-level timestamp yields nothing and would drop the record from any time window.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "27-29",
"snippet": " \"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),\n _ => None,\n }"
}
],
"instrument": "`rg -m1 '\"type\":\"file-history-snapshot\"' <transcript> | jq 'has(\"timestamp\"), .snapshot.timestamp'` must print `false` then an ISO instant. Counting rule: one record inspected.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "SPEC.md sections 4.8 and 5.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg --no-filename '\"type\":\"file-history-snapshot\"' *.jsonl in one project dir piped to a python3 probe counting top-level `timestamp` presence and `snapshot.timestamp` presence and tabulating key sets ; csift search 'file-history snapshot at' -t harness.meta.snapshot <project-dir> --max-count 2 --format json reading ts_utc / ts_local / excerpt ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'insertFileHistorySnapshot.{0,200}'",
"observed": "433 file-history-snapshot lines in the dir. With a top-level `timestamp`: 0. With a non-empty `snapshot.timestamp`: 433. All 433 share one key set: {isSnapshotUpdate, messageId, snapshot, type}; the nested `snapshot` object carries {messageId, timestamp, trackedFileBackups}. The binary's writer is `insertFileHistorySnapshot(e,n,r,o){return this.trackWrite(async()=>{let d={type:\"file-history-snapshot\",messageId:e,snapshot:n,isSnapshotUpdate:r};await this.appendEntry(d,void 0,void 0,o)})}` - no timestamp field is added. csift JSON hits carry ts_utc null and ts_local null with excerpt `[file-history snapshot at 2026-06-25T15:25:52.125Z: AGENTS.md@v2, SKILL.md@v2, ...]`.",
"rule": "One record inspected per raw jsonl line whose top-level type is file-history-snapshot; presence means the key exists at top level (resp. inside the `snapshot` object) with a non-empty value.",
"note": "433 of 433 rather than the claim's single sampled record, and the binary's own writer confirms the omission is by construction rather than an artifact of this corpus. The consequence is directly observable: hit ts_utc and ts_local are null and the nested instant appears only inside the excerpt. Code site verified verbatim at src/model/classify_promoted.rs:22-24."
}
]
},
{
"id": "REC-027",
"area": "record-model",
"behavior": "A `/fork` child's LINE 1 is a timestampless `type:\"fork-context-ref\"` record carrying `parentLastUuid` (the parent's last record uuid at fork time), `contextLength` (messages carried into the fork), `agentId` and `parentSessionId`; the `parentLastUuid` + `contextLength` field pair appears on no other line type. The fork child is written as a SUBAGENT transcript under <session>/subagents/agent-<id>.jsonl with a meta.json reading agentType 'fork' and isFork true - not as a top-level session file (0 of 33 corpus-wide fork-context-ref lines sit in a top-level transcript).",
"depends": "`agents`' head-walk captures fork provenance for free and surfaces the fork point as a re-feedable `show --uuid` address; losing it silently drops the only on-disk link from a fork child to its parent.",
"code": [
{
"path": "src/model/record.rs",
"lines": "52-56",
"snippet": " /// Fork provenance (line 1 of a `/fork` child transcript, `type:\"fork-context-ref\"`):\n /// the PARENT session's last record uuid at fork time - the fork point, feedable to\n /// `csift show @<parent> --uuid <it>`. Absent everywhere else.\n #[serde(default, rename = \"parentLastUuid\")]\n pub parent_last_uuid: Option<String>,"
},
{
"path": "src/subagent/lifecycle.rs",
"lines": "18-20",
"snippet": " // A `/fork` child's LINE 1 is a timestampless `fork-context-ref` record carrying\n // the parent's last uuid at fork time + the carried context length; it precedes\n // the first timestamped record, so the same head walk captures it for free."
}
],
"instrument": "`rg -l '\"fork-context-ref\"' ~/.claude/projects/*/*.jsonl` then `head -1` that file and check it carries `parentLastUuid` and `contextLength` and no `timestamp`. Counting rule = files whose FIRST line has that type.",
"located": {
"claude_code": null,
"csift": null,
"source": "src/model/record.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -l --glob '*.jsonl' '\"fork-context-ref\"' over one project dir ; python3 probe over that dir's subagent transcripts recording the 0-based line index of the fork-context-ref line and its key set ; python3 structural probe splitting every line whose bytes contain 'parentLastUuid' into top-level-field vs payload-text mentions ; csift stats per project dir summing line_types['fork-context-ref'] over kind == 'session' rows ; csift agents @<session> --format json counting agent rows with non-null fork fields ; csift show @<parent-session> --uuid <the parentLastUuid> ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -F 'fork-context-ref' ; sed -n on src/model/record.rs and src/subagent/lifecycle.rs",
"observed": "18 transcripts in the project dir carry a fork-context-ref line; the 0-based line-index histogram is {0: 18}, i.e. all 18 are file line 1. All 18 share the key set {agentId, contextLength, parentLastUuid, parentSessionId, type} - no `timestamp`, no `uuid`, no `message`. Corpus-wide the type totals 33 lines across three project dirs (18 / 14 / 1) and 0 of them sit in a top-level transcript: every one is under <session>/subagents/agent-<id>.jsonl, and the companion meta.json reads agentType 'fork', isFork true, spawnDepth 1. Field exclusivity, structurally: 119 lines in the dir contain the bytes 'parentLastUuid'; exactly 18 carry it as a TOP-LEVEL field and all 18 are fork-context-ref; the other 101 (user 48, assistant 41, attachment 14, queue-operation 2) merely quote the string in payload text. csift agents emits 85 agent rows for that session, 18 of them with non-null fork_parent_last_uuid + fork_context_length (e.g. 7725, 8198) and agent_type 'fork'; feeding one such uuid to `csift show @<parent> --uuid` fetches 1 record unit. The binary carries `fork-context-ref`, `Failed to record fork-context-ref: ` and `[fork-context-ref] parent uuid `.",
"rule": "Counted files = transcripts whose FIRST line has type fork-context-ref. 'Appears nowhere else' is judged structurally: a line counts only if `parentLastUuid` is a top-level JSON key, not if the byte sequence merely occurs in a payload string.",
"note": "Every operative part of the claim reproduces: line 1, timestampless, the two named fields, exclusivity, the head walk capturing it, and the re-feedable show --uuid address. One correction of consequence for anyone re-running the claim's own instrument: it proposes `rg -l '\"fork-context-ref\"' ~/.claude/projects/*/*.jsonl`, which matches NOTHING here, because the fork child is a subagent transcript one directory deeper. The record also carries two fields the claim omits (agentId, parentSessionId). Code sites verified verbatim at src/model/record.rs:51-55 and src/subagent/lifecycle.rs:18-20."
}
]
},
{
"id": "REC-028",
"area": "record-model",
"behavior": "A `compact_boundary` system record carries a `compactMetadata` object and a TOP-LEVEL `logicalParentUuid` naming the true predecessor record the compaction re-links to, while its own `parentUuid` is null. `compactMetadata` carries more than four fields: the dominant shape is {trigger, preTokens, postTokens, durationMs, cumulativeDroppedTokens, preservedMessages, preservedSegment, preCompactDiscoveredTools}.",
"depends": "csift models `logical_parent_uuid` tolerantly and renders it in the boundary excerpt; it is the harness's own ground truth for the post-compaction chain and the only way to rejoin the graph across a boundary.",
"code": [
{
"path": "src/model/record.rs",
"lines": "100-107",
"snippet": " /// compaction points and inspect what each clipped. Absent on every other record. Tolerant.\n #[serde(default, rename = \"compactMetadata\")]\n pub compact_metadata: Option<serde_json::Value>,\n\n /// `logicalParentUuid` (top-level on `compact_boundary` system records): the TRUE\n /// predecessor record the compaction re-links to (`parentUuid` is null on a\n /// boundary) - harness ground truth for the post-compaction chain. Additive +\n /// tolerant."
}
],
"instrument": "`csift search '' @<session> -t harness.compaction.boundary --max-count 1` then read the raw line with `csift show @<session> --line <n> --raw`; expected: `logicalParentUuid` at top level, `parentUuid` null. Counting rule: one boundary record per compaction.",
"located": {
"claude_code": "2.1.231",
"csift": "0.8.1",
"source": "AGENTS.md section 3.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg --no-filename '\"subtype\":\"compact_boundary\"' *.jsonl over one project dir piped to a python3 probe counting compactMetadata presence, non-null top-level logicalParentUuid and null parentUuid, and tabulating both key sets ; csift search '' -t harness.compaction.boundary <project-dir> --max-count 2 ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,180}compactMetadata.{0,260}' and rg -o 'logicalParentUuid.{0,160}' ; sed -n on src/model/record.rs",
"observed": "17 compact_boundary records in the dir. With a compactMetadata object: 17/17. With a non-null top-level logicalParentUuid: 17/17. With parentUuid null: 17/17. compactMetadata key sets: 14 records carry {cumulativeDroppedTokens, durationMs, postTokens, preCompactDiscoveredTools, preTokens, preservedMessages, preservedSegment, trigger}, 1 the same minus preCompactDiscoveredTools, 1 the full set plus `precomputed`. The binary corroborates the wider set - `let{cumulativeDroppedTokens:o,preTokens:d,postTokens:f}=r.compactMetadata`, `r.compactMetadata.preservedMessages?.anchorUuid??r.compactMetadata.preservedSegment?.anchorUuid` - and writes the link as `logicalParentUuid:e.logical_parent_uuid`. csift renders `Conversation compacted [compaction boundary: trigger=auto preTokens=970403 postTokens=22464 durationMs=123871] [logicalParent=<uuid>]`.",
"rule": "One boundary record per compaction; presence is judged on the parsed JSON, and parentUuid counts as null only when the key is present with a JSON null or absent.",
"note": "The three load-bearing facts hold 17/17 with no exception, and the binary shows the logicalParentUuid link is written by the boundary constructor rather than incidental. The correction is the field list: the claim's four are a subset - four more ride along in 16 of 17 records, and csift keeps compactMetadata RAW so the extra fields cost nothing, but a reader treating the four as exhaustive would wrongly conclude the boundary cannot tell you how much was dropped (cumulativeDroppedTokens) or what was preserved (preservedSegment / preservedMessages anchorUuid). Code site verified verbatim at src/model/record.rs:93-100."
}
]
},
{
"id": "REC-029",
"area": "record-model",
"behavior": "Claude Code writes `type:\"user\"` records for far more than human turns: peer-session inbox messages and harness notifications carry the same `type` and `message.role` of `user`, so a naive `type:\"user\"` human-turn filter overcounts by 6.00x corpus-wide (15,014 records versus 2,501 genuine turns) and by 13.14x in the most multi-agent lane.",
"depends": "csift gates the human turn on `Record::is_genuine_user`, which every `-t user.message` selection, the `list` first/last user excerpt and turn segmentation key on; if the shape drifts, `search -t user.message`, `stats` turn counts and `list` rows all silently over- or under-count.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "68-73",
"snippet": " match &msg.content {\n Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n Some(Content::Blocks(blocks)) => {\n let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n if !has_text || has_tool_result {"
}
],
"instrument": "Run `csift search \"\" <target> --count-by label` over a multi-agent session and compare the `user.message` count against `rg -c '\"type\":\"user\"' <the session jsonl>`; counting rule: census rows count RECORDS once per surviving leaf, rg counts LINES. The ratio should exceed 1 and reach ~3x on a corpus-wide run.",
"located": {
"claude_code": null,
"csift": "0.8.2",
"source": "SKILL.md wrong-assumption table (Why not hand-roll this format, row 1)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 probe over all 7,580 transcripts under ~/.claude/projects applying a NAIVE human-turn filter and counting raw type:\"user\" records ; csift search '' --count-by label over the whole corpus for the genuine user.message total ; csift search '' -t user.message --count-by session --format json for the per-transcript genuine counts, joined to the same probe's per-transcript naive counts ; sed -n on src/model/predicates.rs",
"observed": "Corpus-wide: 228,455 records with top-level type \"user\". The naive filter keeps 15,014 of them. csift's label census over the same corpus reports user.message = 2,501 (against agent.tool.result 211,055, agent.communication.inbox 8,651, user.unsent 848, and 2,811 records across the five harness.notification.* leaves). Ratio 15,014 / 2,501 = 6.00x. Per transcript, restricted to the 15 transcripts with at least 20 genuine turns, the worst lane is 644 naive against 49 genuine = 13.14x, then 1,152 against 101 = 11.41x; the median of that set is far lower. src/model/predicates.rs:68-73 holds the quoted match arms verbatim, directly under a comment recording that 106 peer messages were once mislabeled as the user in one real session.",
"rule": "NAIVE filter, stated so a stranger can rerun it: top-level type == 'user' AND message.role == 'user' AND NOT isMeta AND NOT isCompactSummary AND (content is a string OR content is a block array containing a text block and no tool_result block). Genuine = csift's user.message census, which counts records once per surviving leaf. Per-lane ratios are computed only where the genuine denominator is at least 20, so a 1-turn transcript cannot manufacture a headline ratio.",
"note": "The mechanism holds and the failure is now worse, not better, so the claim errs on the safe side. Both numbers moved: 3.03x -> 6.00x corpus-wide and 8.6x -> 13.14x in the worst lane. The driver is visible in the census - agent.communication.inbox alone is 8,651 records, more than three times the entire genuine user.message population, which is what peer-to-peer sessions do to this ratio. Note the naive filter is deliberately generous to the hand-roller: a bare `type:\"user\"` grep would report 228,455, a 91x overcount. Code site verified verbatim at src/model/predicates.rs:68-73."
}
]
},
{
"id": "REC-030",
"area": "record-model",
"behavior": "A `type:\"user\"` record whose `message.content` is a block array containing a `tool_result` block is a tool-result CARRIER, not a human turn (measured 3,361 carriers to 243 prose-shaped records in one transcript, a 13.83x dominance), and 100% of the human turns that ride such a carrier - the `user.answer` and `user.rejection` openers, 106 distinct records corpus-wide - extract as ZERO characters if only `text` blocks are read. Those openers are 4.87% of all human turn openers (128 of 2,629 census records).",
"depends": "csift excludes any block array containing a `tool_result` from `is_genuine_user` and recovers those turns through the `user.answer` / `user.rejection` leaves; drift breaks `search -t user.answer -t user.rejection` and the whole turn-boundary count.",
"code": [
{
"path": "src/model.rs",
"lines": "20",
"snippet": "//! carriers. A genuine user turn is: string content (and NOT `isCompactSummary`),"
},
{
"path": "src/model/predicates.rs",
"lines": "68-73",
"snippet": " match &msg.content {\n Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n Some(Content::Blocks(blocks)) => {\n let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n if !has_text || has_tool_result {"
}
],
"instrument": "`csift search \"\" <target> --count-by label` and compare `user.message` + `user.answer` + `user.rejection` against a raw count of `\"role\":\"user\"` lines; counting rule: records, not blocks. Expect the tool_result carriers to dominate the raw count.",
"located": {
"claude_code": null,
"csift": "0.8.2",
"source": "SKILL.md wrong-assumption table (row 2); AGENTS.md section 3.3"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 probe over the largest top-level transcript in one project dir classifying every type:\"user\" record as tool_result carrier vs prose-shaped ; csift search '' @<session> --no-subagents --count-by label on that same transcript ; csift search '' -t user.answer -t user.rejection --count-by label and the same query with --raw --max-count 0 corpus-wide, piped to a python3 probe extracting only `type:\"text\"` blocks from message.content ; the same query with --format json to count hit records and distinct (session, line) addresses ; sed -n on src/model.rs and src/model/predicates.rs",
"observed": "One transcript: 3,604 records with type \"user\" = 3,361 tool_result carriers vs 243 prose-shaped, a 13.83x carrier dominance; csift labels 77 of them user.message and 14 user.unsent, with agent.tool.result at 3,361. Corpus-wide the answer/rejection census is user.answer 118 + user.rejection 10 = 128 records, which resolve to 106 distinct (session, line) addresses; `--raw` emits exactly those 106 physical lines and 106 of 106 (100%) yield ZERO characters when only `type:\"text\"` blocks of message.content are read - none yielded even one character. As a share of human turn openers that is 128 / (2,501 + 128) = 4.87%. src/model.rs:20 is `//! carriers. A genuine user turn is: string content (and NOT \\`isCompactSummary\\`),`, sitting under a comment recording 332 + 61 genuine against 1619 carriers; src/model/predicates.rs:68-73 holds the quoted arms.",
"rule": "Records, not blocks. A record is a tool_result CARRIER iff its message.content is a block array containing at least one tool_result block; prose-shaped iff string content or a block array with a text block and no tool_result. Zero-extract is judged on the verbatim line: concatenate the `text` fields of every `type:\"text\"` block in message.content and measure the character length. 128 census records collapse to 106 physical lines because a record selected by more than one view is counted once per surviving leaf by the census but emitted once by --raw.",
"note": "The qualitative claim is not merely intact, it is absolute where the claim was statistical: not 'some' but every single one of the 106 answer/rejection records extracts as zero characters from text blocks, because the answer rides in the tool_result payload and there is no text block at all. Two numbers moved. The carrier ratio in the transcript I measured is 13.83x rather than the 1,619-to-393 (4.1x) recorded in the src/model.rs comment - that comment's session is a real, older measurement, so this is corpus drift toward MORE carriers, not a contradiction. The 16.5% share is now 4.87%, because the denominator (user.message) grew far faster than AskUserQuestion and plan-rejection turns did. Code sites verified verbatim at src/model.rs:20 and src/model/predicates.rs:68-73."
}
]
},
{
"id": "REC-031",
"area": "record-model",
"behavior": "A subagent transcript's first record is an isSidechain:true type:\"user\" seed carrying the spawn prompt in 7483 of 7516 measured transcripts (99.56%). The other 33 open with a type:\"fork-context-ref\" line - a context-fork spawn shape carrying {agentId, contextLength, parentLastUuid, parentSessionId} and no message - after which the first real record is an isSidechain:true assistant; 31 of those 33 carry no user seed anywhere. In TOP-LEVEL transcripts a sidechain record does not occur at all (0 lines across 64 transcripts).",
"depends": "`is_genuine_user` deliberately does NOT gate on `isSidechain`, so the `list` per-subagent preview can treat the seed as the subagent's first user message; the seed classifies `agent.communication.inbox` (parent to self), not `user.message`. Since v0.10.2 a subagent transcript whose line 1 is a `fork-context-ref` record has NO spawn seed: its openers are the parent's own human messages and classify `user.message`; only a genuine child's first opener is the parent-to-self seed.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "48-53",
"snippet": " // NOTE on `isSidechain`: a subagent transcript's FIRST record is an\n // `isSidechain:true` user seed. It is NOT gated out here on purpose - `list`'s\n // per-subagent preview legitimately treats that seed as the subagent's \"first\n // user message\", and in TOP-LEVEL transcripts a sidechain seed does not occur in\n // any real corpus. Gating it would silently blank the subagent preview for zero\n // real benefit; the per-surface scan owns subagent-vs-parent context instead."
},
{
"path": "src/model/classify.rs",
"lines": "330-335",
"snippet": " // The spawn-prompt seed of a subagent transcript is an inbound comm (parent ⇨ self),\n // not the operator (GOLD §3) - unchanged, regardless of isMeta.\n if ctx.is_subagent && ctx.is_transcript_opener {\n push_unique(out, Class::CommInbox);\n return;\n }"
},
{
"path": "src/search/turns_match.rs",
"lines": "63-70",
"snippet": " let first_opener_line = if head_is_fork {\n None\n } else {\n records\n .iter()\n .find(|k| k.rec.opens_turn())\n .map(|k| k.line_no)\n };"
},
{
"path": "src/search/scan.rs",
"lines": "254-255",
"snippet": " let head_end = memchr::memchr(b'\\n', bytes)\n .unwrap_or(bytes.len())"
}
],
"instrument": "`head -1 <a subagent transcript> | rg -c isSidechain` (expect 1) and `rg -c '\"isSidechain\":true' <a top-level transcript>` (expect 0). Counting rule = physical lines carrying the flag, per lane.",
"located": {
"claude_code": null,
"csift": null,
"source": "src/model/predicates.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 scan of ~/.claude/projects: for every */*/subagents/**/*.jsonl (journal.jsonl excluded) read line 1 and bucket (type, isSidechain, content-is-string); for every */*.jsonl count physical lines containing b'\"isSidechain\":true'. Plus: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'fork-context-ref'. Plus: csift show @<agent-id> --line 1 --format json on a subagent whose line 1 is the seed.",
"observed": "7516 subagent transcripts scanned: 7483 open with ('user', True, True) and 33 open with ('fork-context-ref', None, False). 64 top-level transcripts scanned: 0 files with any isSidechain:true line, 0 such lines total. Of the 33 fork-context-ref openers, only 2 carry a user isSidechain string seed anywhere in the file. Binary 2.1.258 matches 9 strings for fork-context-ref, including the literal 'Failed to record fork-context-ref: ' and '\"fork-context-ref\":\"route-by-agent\"'. A fork-context-ref opener's own record fields are {agentId, contextLength, parentLastUuid, parentSessionId, type} and the record after it is type:assistant, isSidechain:true. On a normal subagent, csift show --line 1 gives labels ['agent.communication.inbox'] with a parent-to-self direction.",
"rule": "Per-lane count of physical first lines (subagent lane) and of physical lines carrying the flag (top-level lane); one row per transcript file.",
"note": "csift is not broken by the fork shape: on a fork-context-ref transcript, csift show --line 1..3 books the opener as non_record_lines:1 with skipped_lines:0 and does not hand the following assistant record a spurious opener label (it classified agent.tool.use + agent.communication.sent). The claim's corollary that a subagent's first record can be treated as its first user message therefore has a 33-transcript exception class where there is no seed record at all."
},
{
"claude_code": "2.1.258",
"csift": "0.10.2",
"date": "2026-09-03",
"verdict": "drifted",
"instrument": "corpus census of subagent transcripts whose line 1 contains `fork-context-ref`, counting those that carry a genuine user record (role user, string content, no tool_result, not isMeta); e2e on 0.10.2 with a fork clone beside a genuine child",
"observed": "42 fork transcripts, 10 of them with a genuine user record - on 0.10.1 that record's first opener classified agent.communication.inbox (the positional seed rule); on 0.10.2 the clone's opener classifies user.message and the genuine child keeps its seed",
"rule": "one file = one clone; a clone with any genuine user record is a reachable mislabel",
"note": "fix: search/scan.rs reads the file's first line for the fork marker and turns_match assigns no seed to a clone; e2e a_fork_clone_has_no_spawn_seed_but_a_genuine_child_does"
}
]
},
{
"id": "REC-032",
"area": "record-model",
"behavior": "Same mechanism, re-measured numbers: 0 of 856 superseded drafts appear in preservedMessages (the ledger and the src/model/taxonomy.rs doc comment both say 0 of 772, a smaller earlier corpus). The exclusion is discriminating rather than vacuous: 11 of 560 resend siblings DO appear in the same preserved sets.",
"depends": "csift assigns the draft the single leaf `user.unsent` at the scan layer (a per-record classify cannot see the later sibling), keeps it outside turn numbering, and uses the `preservedMessages` exclusion as the LLM-visibility instrument for a bare `-t user`; a recalled-then-abandoned message has no sibling and is structurally undetectable. Since v0.10.2 a superseded record never fans out into per-section classes: the single `user.unsent` view is emitted whatever the draft's text shape, so a hit's `label` is always a member of its own `labels[]` and a notification selector never sees a draft.",
"code": [
{
"path": "src/search/hits.rs",
"lines": "168-176",
"snippet": " // A superseded turn-opener draft (the scan layer computed the set - a pure\n // per-record classify cannot see the LATER same-parent sibling) carries the single\n // label `user.unsent`, whatever kind of opener it was: the draft is not part of the\n // conversation, so it never rides `user.message` (whose counts stay pure).\n let labels = if superseded {\n vec![Class::UserUnsent]\n } else {\n rec.classify(ctx)\n };"
},
{
"path": "src/search/turns_match.rs",
"lines": "214-224",
"snippet": " // C-18 + user.unsent: a superseded draft is a real record OUTSIDE turn numbering.\n // A scan emits it as its own annotated unit when it hits (searchable, labeled\n // `user.unsent`) - except under a `--turn` window, which asks about NUMBERED turns\n // and a draft belongs to none. An explicit address always reaches it (refetch law).\n if address.is_some() || turn_bounds.is_none() {\n let mut draft_idxs: Vec<usize> = skip.iter().copied().collect();\n draft_idxs.sort_unstable();\n for i in draft_idxs {\n out.extend(build_exchange(0, &[i], true));\n }\n }"
},
{
"path": "src/model/taxonomy.rs",
"lines": "182-196",
"snippet": " /// glob form and explicit paths reach the invisible ones.\n #[must_use]\n pub fn llm_visible(self) -> bool {\n !matches!(\n self,\n Class::UserUnsent\n | Class::UserQueued\n | Class::CompactionBoundary\n | Class::MetaTurnDuration\n | Class::MetaAwaySummary\n | Class::MetaStopHooks\n | Class::MetaSnapshot\n | Class::MetaSystem\n )\n }"
},
{
"path": "src/search/hits.rs",
"lines": "257-261",
"snippet": " let sections = if superseded {\n Vec::new()\n } else {\n rec.record_text_sections(ctx)\n };"
}
],
"instrument": "compactMetadata.preservedMessages is a JSON OBJECT {anchorUuid, uuids:[...], allUuids?:[...]}, not an array. Treating it as a list of uuids yields 0 preserved uuids and makes the instrument vacuously pass; the object shape is confirmed in the 2.1.258 binary, which builds 'preservedMessages:{anchorUuid:r.anchor_uuid,uuids:[...r.uuids],...r.all_uuids!==void 0&&{allUuids'.",
"located": {
"claude_code": null,
"csift": "0.9.2",
"source": "CHANGELOG 0.9.2 (user.unsent); CHANGELOG 0.9.4 (LLM-visibility instrument)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over all 64 top-level transcripts: group non-meta non-summary non-tool_result user records by parentUuid, call all but the last in each multi-member group a superseded draft, and intersect their uuids with the union of compactMetadata.preservedMessages.uuids / .allUuids / .anchorUuid from every compact_boundary record in the same file. Cross-check: csift search \"\" -t user.unsent --no-subagents --format json | tail -1, and csift show @<session> --line <draft> --raw.",
"observed": "64 transcripts, 229 compact_boundary records, 3785 preserved uuids collected. 560 parentUuid groups with more than one user opener; 856 superseded drafts; 0 of the 856 appear in any preservedMessages uuid set, while 11 of the 560 resend siblings do. csift's own count over the same scope: matched 848 user.unsent records across 26 sessions. A sampled draft renders label user.unsent, superseded_draft true, turn_index null, and it plus its resend two lines later share one parentUuid.",
"rule": "One draft per resend sibling (all but the last member of a parentUuid group); intersection counted per uuid, per file.",
"note": "csift's 848 and the python 856 differ because csift's draft set is derived from opens_turn (which also admits answer/rejection/peer openers and excludes slash-command wrappers) while the python approximation keys on plain user records; both agree on 0 overlap with preservedMessages."
},
{
"claude_code": "2.1.258",
"csift": "0.10.2",
"date": "2026-09-03",
"verdict": "drifted",
"instrument": "code reading of search/hits.rs at 0.10.1 (the sections branch ran for a superseded record and emitted the section class with the forced labels vector) plus an e2e on 0.10.2: a pulse-shaped user record sharing its parentUuid with a later human message, `search -t user.unsent --format json` and `search -t harness.notification -c`",
"observed": "0.10.1 path: class harness.notification.background-command carried labels [user.unsent]; 0.10.2: one hit with label and labels both user.unsent, the notification selector counts 0",
"rule": "a hit's label must be a member of its own labels[]",
"note": "fix: search/hits.rs computes no sections for a superseded record; e2e a_sectioned_draft_keeps_the_single_unsent_label"
}
]
},
{
"id": "REC-033",
"area": "record-model",
"behavior": "A conversation fork, rewind, retry or parallel lane leaves exactly one plain DAG fact: some record has MORE THAN ONE conversation child (a later `parentUuid` re-attach). Which side is live is NOT computable from the jsonl, because a parallel tool fan-out leaves the same shape; tool-result carriers, isMeta records and compaction summaries are never conversation children.",
"depends": "`show --branch-points` reports the fork FACTS ranked by the widest inter-child time gap and never classifies; a live/abandoned classifier was prototyped and REFUTED against real corpora because parallel tool fan-out false-positives as abandoned branches on most sessions.",
"code": [
{
"path": "src/show/branch.rs",
"lines": "3-7",
"snippet": "//! A Claude Code rewind, retry, or parallel lane leaves one plain DAG fact: some record\n//! has MORE THAN ONE conversation child (a later `parentUuid` re-attach). Which side is\n//! \"live\" is NOT computable from the jsonl: a live/abandoned classifier was prototyped\n//! and refuted against real corpora (parallel tool fan-out makes sibling leaves that\n//! false-positive as abandoned branches on most sessions)."
}
],
"instrument": "`csift show @<uuid> --branch-points --format json | jq 'select(.kind==\"branch-point\") | {uuid, line, widest_gap_seconds, children: [.children[].line]}'`; counting rule = one row per record with two or more conversation children, where a conversation child is a user/assistant record that is not a tool_result carrier, not isMeta and not a compaction summary.",
"located": {
"claude_code": null,
"csift": "0.8.1",
"source": "CHANGELOG 0.8.1 (show --branch-points); src/show/branch.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift show @<session> --branch-points --format json on the largest top-level transcript (415 MB) and on an 8.9 MB one, versus an independent python recount over the same 8.9 MB file that groups records by parentUuid after dropping non-user/assistant records, isMeta, isCompactSummary and any user record carrying a tool_result block.",
"observed": "415 MB transcript: 319 branch-point rows, widest_gap_seconds min 0 / median 31 / max 14197, and 120 of the 319 have a widest gap below 2 seconds. 8.9 MB transcript: python recount = 4 branch points with child-line sets [[10,40],[57,60],[1569,1575,1582],[1845,1847,1849]]; csift emitted exactly 4 rows with exactly those child-line sets, ordered by widest gap 201 / 45 / 40 / 29 seconds. Emitted keys are exactly ['children','kind','line','uuid','widest_gap_seconds'] - no live/abandoned field on any row.",
"rule": "One row per record with two or more conversation children; a conversation child is a user/assistant record that is not a tool_result carrier, not isMeta and not a compaction summary.",
"note": "The independent recount reproduced csift's rows exactly, and the near-zero-gap population (120 of 319, 38%) is direct evidence for the refutation the claim records: a classifier keyed on sibling-ness would call that whole third of the rows abandoned branches when they are parallel tool fan-out."
}
]
},
{
"id": "REC-034",
"area": "record-model",
"behavior": "Mechanism confirmed exactly, including that the real numbers survive in usage.iterations on the zeroed copy. Numbers re-measured: across 5 large transcripts, 3 had 0 mismatching ids and 2 had exactly 1 each (not 3 in one session). A SECOND disagreement shape exists that the claim does not describe: a non-zeroed, non-uuid-duplicated pair where the first record carries a smaller output_tokens (2) than its siblings (1404) - a partial first record rather than a compaction replay. A per-field MAX is correct for both shapes; first-wins would be wrong for both, and in the second shape wrong in the direction of under-reporting.",
"depends": "csift takes a per-field MAX rather than first-wins, which is identical on clean data, immune to the zeroed replay, and commutative so the rayon fold stays correct.",
"code": [
{
"path": "src/stats.rs",
"lines": "252-256",
"snippet": " // Dedupe per FILE by message.id, taking the per-field MAX across the\n // id's admitted records: identical on clean data, and immune to the\n // compaction-replay shape where a replayed copy carries ZEROED usage\n // (first-wins would depend on traversal order). An id-less record\n // counts on its own, as before."
}
],
"instrument": "Group a large transcript's records by `message.id` and flag ids whose members disagree on any usage field; counting rule = per id, per file. Measured: 2 of 3 sampled sessions had 0 mismatches, one large session had 3 mismatching ids, each a duplicate-uuid replay whose second copy was all-zero.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.2",
"source": "src/stats.rs comment; dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over the 5 largest top-level transcripts under 300 MB: group records by message.id, build the tuple (input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens) per record, and flag ids with more than one distinct tuple; for each flagged id report whether any copy is all-zero and whether the copies share a uuid. Then read the two disagreeing lines directly.",
"observed": "415 MB file: 7538 distinct message.ids, 1 id with more than one distinct usage tuple - 6 records, three at (2,708,474834,3997) and three at (0,0,0,0), sharing uuids across the two triples. 298 MB file: 12693 ids, 0 mismatches. 119 MB file: 6208 ids, 1 mismatch, NOT zeroed and NOT uuid-duplicated - (2,2,20853,80392) versus (2,1404,20853,80392). 99 MB file: 2906 ids, 0 mismatches. Direct read of the two zeroed-versus-real lines: identical uuid, identical timestamp 2026-06-06T08:47:12.527Z, DIFFERENT parentUuid, top-level usage all zero on the replay, and usage.iterations[0] on BOTH copies carrying the real (2,708,474834,3997).",
"rule": "Per message.id, per file: count ids whose member records disagree on any of the four usage fields.",
"note": "The two shapes together make the ordering-independence argument stronger than the claim states: neither first-wins nor last-wins is safe, because the bad copy is first in one shape and second in the other."
}
]
},
{
"id": "REC-035",
"area": "record-model",
"behavior": "Mechanism confirmed with a sharper signature than the claim gives: the child copy keeps the parent's cache_read_input_tokens byte-identical while carrying a near-zero output_tokens, which is what identifies it as the spawning message replayed into the child's opening context rather than an independent generation. Numbers re-measured on this corpus: 2 of 25 sessions affected; 32 shared (id,file) pairs over 20 ids; the largest single session's residual double-count is 14 records / 29419 output / 1586046 cache-read (the ledger's 4 ids / 12 files / 12 records / 19884 output / 7401557 cache-read describes a different session).",
"depends": "csift scopes the usage dedupe PER FILE (one row per transcript, the TOTAL merging rows), because each copy is a genuine per-transcript fact; a pooled dedupe would have to pick arbitrarily between the parent and child values and would under-report a spanning `stats` total.",
"code": [
{
"path": "src/stats.rs",
"lines": "337-340",
"snippet": "/// Scope law: usage dedupe is PER FILE (each row already deduped by message.id). The\n/// same id CAN recur across a session's transcripts - the spawn message is copied into\n/// each child's opening context with its own usage - and each copy is a genuine\n/// per-transcript fact, so the TOTAL row sums them; it never dedupes across files."
}
],
"instrument": "Collect `message.id` -> (file, usage) pairs across one top-level transcript and its `subagents/` tree; counting rule = per (id, file). Expect a small number of ids present in both domains with differing usage. Measured: 4 ids shared across 12 subagent files in one session; the residual double-count in the scope TOTAL was 12 records / 19,884 output / 7,401,557 cache-read.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.2",
"source": "SPEC.md section 6 v0.9.2 ledger item 2; src/stats.rs comment; dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over every top-level transcript under 250 MB that has a sibling subagents/ tree: build message.id -> (record count, max output_tokens, max cache_read_input_tokens) separately for the parent file and for each subagent transcript, then intersect the parent's id set with each child's.",
"observed": "25 sessions with a subagents tree scanned. 2 of them carry at least one message.id present in BOTH the parent and a subagent transcript: 32 (id,file) shared pairs over 20 distinct ids, of which 15 pairs disagree with the parent on max output_tokens. In the 15-subagent session, the largest divergences were parent 4 records / 5598 output versus child 1 record / 4 output, and 4 / 5507 versus 1 / 4 - with cache_read_input_tokens IDENTICAL across the two sides (51101 and 77496 respectively). Residual that per-file dedupe leaves in that session's scope TOTAL: 14 records, 29419 output tokens, 1586046 cache-read tokens.",
"rule": "Per (message.id, file) pair; a pair counts as shared when the id appears with a usage object in both the parent transcript and one subagent transcript.",
"note": "The claim's design conclusion is unaffected: because the two sides genuinely differ, a pooled cross-file dedupe would have to choose between 5598 and 4 output tokens for one id, and either choice misstates one of the two transcripts."
}
]
},
{
"id": "REC-036",
"area": "record-model",
"behavior": "Transcript string content is JSON-encoded on the wire: a literal `\"` becomes `\\\"` and every control character below 0x20 plus DEL becomes an escape such as `\\n` or `\\uXXXX`, so a rendered `hello world` may be raw `hello\\nworld` and a rendered quote is never a raw quote - rendered text differs byte-for-byte from the raw line wherever whitespace or an escapable character sits.",
"depends": "csift derives literal prefilter needles necessity-only and applies a per-needle safety predicate (no whitespace, no JSON-escaped character, at least 3 bytes); a needle failing it would silently drop real matches, which is the one place the no-silent-truncation contract actually bites.",
"code": [
{
"path": "src/search/matcher.rs",
"lines": "338-342",
"snippet": " // A char that JSON escapes inside a string does not survive verbatim in the raw\n // line bytes the prefilter scans - emitting a literal for it causes false\n // negatives. `\\` is already excluded via META above; guard `\"`, control chars,\n // and DEL here.\n if pattern.chars().any(json_escapes_in_string) {"
},
{
"path": "src/search/matcher.rs",
"lines": "535-537",
"snippet": "/// True when `c` is escaped inside a JSON string literal (so it never appears\n/// verbatim in the raw line bytes): `\"`, any C0 control char (`< 0x20`), or DEL.\n/// (`\\` is handled separately as a regex metacharacter.)"
}
],
"instrument": "Compare `csift search 'hello world' @<session> -c` against `csift search 'hello\\s+world' @<session> -c` on a transcript whose text carries a newline between the words; the counts must agree with a `jq -r '.message.content[]?.text' <file> | grep -c` baseline. Counting rule: matched records with and without the whitespace in the pattern.",
"located": {
"claude_code": null,
"csift": "0.9.4",
"source": "AGENTS.md section 4; SPEC.md section 7d"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Live corpus: python3 read of one assistant/user text block containing an embedded newline plus a byte-level test of the carrying raw line. Fixture: a --claude-home tree holding a compact-JSON transcript whose user content is 'alpha\\nbravo and a quote \" here' and whose assistant text is 'charlie\\ndelta', then csift --claude-home <fixture> search '<pattern>' -c for four patterns.",
"observed": "Live raw line: 37657 bytes, 343 occurrences of the two-byte sequence backslash-n, 388 of backslash-quote, and ZERO real 0x0A inside the line. The rendered pair 'instruction' + newline + 'agent' is present in the raw bytes only as 'instruction\\\\nagent'; the rendered form with a space is absent. Fixture counts: 'alpha\\s+bravo' -> 1, 'alpha bravo' -> 1, 'quote \" here' -> 1, 'charlie\\s+delta' -> 1.",
"rule": "Byte-occurrence counts within one raw line; and matched-record counts (csift -c) per pattern against a fixture with exactly one matching record each.",
"note": "Every pattern that a naive whitespace or quote needle would have gated away still returns its record, which is the property the per-needle safety predicate exists to guarantee. Note that a literal-space pattern also matched across a rendered newline, so the render-normalization path the claim mentions is live as well as the prefilter guard. Both matcher.rs snippets (338-342 and 526-528) are verbatim in the current file."
}
]
},
{
"id": "REC-037",
"area": "record-model",
"behavior": "EVERY `tool_use` block's matchable text is its `name` plus the RE-SERIALIZED JSON `input`, so a real newline inside for example a Bash `input.command` is already the two-character sequence backslash-n by match time.",
"depends": "A csift regex must match the literal backslash-n there and `--multiline` is correctly irrelevant; `--multiline` helps only where rendered text keeps real newlines (message text, thinking, tool_result bodies).",
"code": [
{
"path": "src/search/record_text.rs",
"lines": "374-385",
"snippet": "/// Render a `tool_use` block to searchable text: `name {json-input}`. The name is\n/// matched first so `csift search AskUserQuestion -t agent.tool.use` works; the input JSON is\n/// included so a regex can match arguments too.\npub(crate) fn render_tool_use(name: Option<&str>, input: Option<&serde_json::Value>) -> String {\n let mut s = String::new();\n if let Some(n) = name {\n s.push_str(n);\n }\n if let Some(v) = input {\n s.push(' ');\n s.push_str(&v.to_string());\n }"
}
],
"instrument": "`csift search 'cd /tmp\\\\n' <target> -t agent.tool.use` (hits) versus the same pattern with a real newline under `--multiline` (no hits). Counting rule: matched records.",
"located": {
"claude_code": null,
"csift": "0.6.10",
"source": "CHANGELOG 0.6.10; SKILL.md search --multiline caveat"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Fixture transcript carrying one tool_use block whose input.command is the REAL two-line string 'cd /tmpfx' newline 'echozulu', then: csift --claude-home <fixture> search 'cd /tmpfx\\\\nechozulu' -t agent.tool.use -c versus the same words with an actual newline in the pattern under --multiline.",
"observed": "Raw line carries the bytes 'cd /tmpfx\\\\nechozulu' (verified true before searching). Literal two-character backslash-n pattern: 1 matched record. Real newline in the pattern with --multiline: 0 matched records.",
"rule": "Matched-record count (csift -c) per pattern against a fixture holding exactly one tool_use record.",
"note": "The asymmetry is exactly as claimed and is a consequence of render_tool_use serializing input with Value::to_string, so the JSON escape is what reaches the matcher. src/search/record_text.rs 351-362 is verbatim in the current file."
}
]
},
{
"id": "REC-038",
"area": "record-model",
"behavior": "Claude Code's own writers emit compact JSON (`\"role\":\"user\"`), but a line reserialized with whitespace around the colon (`\"role\": \"user\"` - a default JSON dump, a jq or editor round-trip) is valid JSON and the SAME record.",
"depends": "csift's stage-1 candidate keeps use the serialization-tolerant matchers `line_has_role_marker` / `line_has_user_role_marker`; the old exact compact byte pair silently DROPPED a reserialized line one layer BEFORE any malformed counter could see it - no preview, no count, no match, zero disclosure. Every other prefilter needle must be a bare value substring or a key-only form, never a compact `\"key\":\"value\"` pair.",
"code": [
{
"path": "src/parse/lines.rs",
"lines": "42-56",
"snippet": "/// Serialization-tolerant role-marker test - THE stage-1 candidate needle for\n/// message records (R13). CC's own wire format is compact JSON, but a hand-authored\n/// or reserialized line may carry whitespace around the colon (`\"role\": \"user\"`) -\n/// valid JSON, the same record. The old exact-byte needles (`\"role\":\"user\"`)\n/// silently DROPPED such lines one layer BEFORE any malformed counter could see\n/// them: not skipped, not counted, simply invisible on every surface. One `memmem`\n/// pass for the quoted key + an O(1) verify per hit; a keyless line costs ONE scan\n/// where the old disjunct cost two, so §7 holds (benchmarked on the real corpus).\n/// The quoted-key needle cannot match content text: inside a JSON string the quotes\n/// are escaped (`\\\"role\\\"`), which breaks the needle bytes.\npub(crate) fn line_role_value_matches(\n line: &[u8],\n accept_user: bool,\n accept_assistant: bool,\n) -> bool {"
},
{
"path": "src/parse/lines.rs",
"lines": "87-98",
"snippet": "/// `\"role\"` is `\"user\"` OR `\"assistant\"` (any JSON whitespace around the colon) -\n/// the candidate test for `search`/`show`/`verbatim`/`list`/`stats` stage-1 filters.\npub fn line_has_role_marker(line: &[u8]) -> bool {\n line_role_value_matches(line, true, true)\n}\n\n/// `\"role\"` is `\"user\"` only - the genuine-user/carrier hook `files`/`recover` use\n/// (their assistant-side coverage rides tool-name needles, so admitting every\n/// assistant text record here would repeal their §7 prefilter).\npub fn line_has_user_role_marker(line: &[u8]) -> bool {\n line_role_value_matches(line, true, false)\n}"
},
{
"path": "src/search/scan.rs",
"lines": "373-378",
"snippet": " // v0.10.1 catch-all system subtypes: the key-only needle `\"subtype\"` (every system\n // record carries it; a quoted KEY survives a reserialize, and the classify arm\n // decides which subtype it is - the already-modeled ones simply reclassify).\n static SUBTYPE_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n std::sync::LazyLock::new(|| memmem::Finder::new(b\"\\\"subtype\\\"\"));\n crate::parse::line_has_role_marker(line)"
}
],
"instrument": "Write a fixture transcript whose lines carry `\"role\": \"user\"` (a default `json.dumps`) into a `--claude-home` tree and confirm `csift search` finds its content and `--count-by label` counts the record. Counting rule: matched records for one known needle, expected 1. The regression is pinned in the `search` e2e modules under `tests/cli/`.",
"located": {
"claude_code": null,
"csift": "0.6.9",
"source": "AGENTS.md section 3.3a; SPEC.md section 7d; src/parse/lines.rs comment; AGENTS.md section 3.3a R13 needle law; CHANGELOG 0.6.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Fixture transcript written with python3 json.dumps at DEFAULT separators (so the wire form is '\"role\": \"user\"'), then: grep -c '\"role\":\"user\"' <file>; grep -c '\"role\": \"user\"' <file>; csift --claude-home <fixture> search 'needlefoxtrot' -c; csift --claude-home <fixture> search 'needlefoxtrot' --count-by label.",
"observed": "File is one line, 299 bytes. Compact byte-pair needle: 0 hits. Spaced form: 1 hit. csift search -c: 1. csift --count-by label: '1 user.message', summary '1 matched record(s) across 1 label key(s)'.",
"rule": "grep -c counts matching LINES; csift -c counts matched records. The fixture holds exactly one record, so the expected value is 1 in both csift modes and 0 for the compact needle.",
"note": "The counterfactual is the load-bearing half and it reproduced: the compact byte pair scores zero on a record that is unambiguously a user message, so a prefilter built on it would drop the record with no malformed count and no disclosure. Only the code coordinates needed correction."
}
]
},
{
"id": "REC-039",
"area": "record-model",
"behavior": "The `Full output saved to:` and `persistedOutputPath` markers name content that lives OUTSIDE the transcript bytes, so text matched after resolution is by definition not a substring of the raw jsonl line.",
"depends": "csift registers both as CONSERVATIVE synth markers that force a full scan when `--resolve-persisted` is on, because the byte prefilter would otherwise silently drop every match that lives only in the external file - the exact silent-truncation class the prefilter laws exist to prevent.",
"code": [
{
"path": "src/search/matcher.rs",
"lines": "522-526",
"snippet": " let mut conservative: Vec<&[u8]> = vec![b\"To tell you how to proceed\"];\n if args.resolve_persisted {\n conservative.push(b\"persistedOutputPath\");\n conservative.push(b\"Full output saved to:\");\n }"
}
],
"instrument": "Read the csift unit tests beside `src/search/` that pin `synth_marker_finders` under `--resolve-persisted`; expect the two conservative needles present only when the flag is set. Counting rule: one needle per pushed literal.",
"located": {
"claude_code": null,
"csift": "0.6.0",
"source": "AGENTS.md section 4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Two fixture transcripts, each with a tool_result pointing at an external tool-results file whose content holds a needle VERIFIED absent from the transcript bytes. Fixture A carries toolUseResult.persistedOutputPath; fixture B carries only the inline 'Full output saved to: <path>' line and no persistedOutputPath field. Then csift --claude-home <fixture> search '<needle>' -c with and without --resolve-persisted.",
"observed": "Fixture A: needle in transcript bytes = False; without the flag 0 matched records, with --resolve-persisted 1. Fixture B: 'persistedOutputPath' absent from the file = True, needle in transcript bytes = False; without the flag 0, with --resolve-persisted 1. Control needle that IS in the transcript ('persisted-output'): 1 either way.",
"rule": "Matched-record count (csift -c) per (fixture, flag) cell; each fixture holds exactly one resolvable record, so the expected cells are 0 without the flag and 1 with it.",
"note": "This is a behavioural check rather than the test-reading the ledger proposed, and it is stronger: the needle appears nowhere in the file's bytes, so the stage-1 byte prefilter must have gated the whole file out unless the conservative marker forced the full scan. Both markers were exercised independently, so neither is dead. src/search/matcher.rs 513-517 is verbatim, including the args.resolve_persisted gate."
}
]
},
{
"id": "REC-040",
"area": "record-model",
"behavior": "The growth half is measured and holds. The torn-fragment half was NOT witnessed in the live corpus: 0 of 7607 transcripts end without a trailing newline at rest, so Claude Code's appends are line-atomic at these sizes and the torn tail is a defensive property rather than an observed shape. Shrink was not exercised.",
"depends": "csift does not silently skip a torn final fragment - it COUNTS it. A 120-byte truncated final line is reported as skipped_lines: 1 on both search and the stats whole-file census, with exit 0 and the complete records still matched. The accurate statement is that csift never crashes or mis-parses on a torn tail and always discloses it, which is the no-silent-failure contract rather than a skip.",
"code": [
{
"path": "src/parse/lines.rs",
"lines": "101-109",
"snippet": "pub(crate) fn line_payload(line: &[u8]) -> Option<&[u8]> {\n let line = line.strip_suffix(b\"\\n\").unwrap_or(line);\n let line = line.strip_suffix(b\"\\r\").unwrap_or(line);\n if line.iter().all(u8::is_ascii_whitespace) {\n None\n } else {\n Some(line)\n }\n}"
}
],
"instrument": "Run `csift search '' @main` repeatedly against the currently-writing session while it works; every run must exit 0 with a stable `skipped_lines` count. Counting rule: `skipped_lines` from the JSON summary across repeated runs.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 7a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) Growth: a background python3 appender writing 300 records at 20 ms intervals with flush+fsync into a fixture transcript, while 10 successive csift --claude-home <fixture> search 'growneedle' @<prefix> -c runs (plus a --format json run per iteration for skipped_lines) scan the same growing file. (b) Torn line: a fixture whose final line is a JSON record truncated to 120 bytes with NO trailing newline, read by csift search --format json and by csift stats --format json. (c) Corpus census: seek to the last byte of every *.jsonl under ~/.claude/projects (journal.jsonl excluded) and count files whose final byte is not 0x0A.",
"observed": "(a) file size climbed 0 -> 1456 -> 3245 -> 5039 -> 7132 -> 8926 -> 10720 -> 13112 -> 14906 -> 16700 bytes across the 10 scans; every scan exited 0; matched rose monotonically 0,5,12,18,24,30,37,44,50,56; skipped_lines was 0 on all 10; the post-run scan matched all 300 records at 89856 bytes. (b) exit 0, the complete record still matched, and the torn fragment was reported as skipped_lines: 1 by BOTH search and stats (stats line_types {'user': 1}, turns 1). (c) 7607 transcripts sampled, 0 zero-byte, 0 whose final byte is not a newline.",
"rule": "(a) one scan per iteration, comparing exit code, matched count and skipped_lines against the file size read immediately before. (b) skipped_lines from the JSON summary. (c) one row per transcript file, final byte only.",
"note": "What would decide the unwitnessed halves: sampling a transcript's final byte in a tight loop while a session streams a very large tool result would catch a partial write if one is ever observable; and a truncate-under-open-mmap test would decide the shrink half, which no instrument here exercised."
}
]
},
{
"id": "REC-041",
"area": "record-model",
"behavior": "A cloned transcript's copied records are REWRITTEN, not byte-copied: each carries the CLONING build's `version`, not the version the origin stamped on that same record. `uuid` and `timestamp` survive byte-identically, while `version`, `sessionId` and `sessionKind` are rewritten and `slug` is stripped. Measured on the one clone/origin pair in the corpus: all 558 shared uuids read 2.1.251 in the origin and 2.1.252 in the clone (0/558 equal), with identical timestamps, `slug` present->absent and `sessionKind` null->\"bg\".",
"depends": "csift reports `version_first` from the head window and `version` from the tail window, so a clone reports the clone-time build as its first - one of the visible symptoms of the fork.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "134-138",
"snippet": " // The base fields are LAST-seen (what the session is on NOW); the head\n // capture becomes the *_first pair. Either window can be empty - fall back\n // to the other so a one-record session reports the same value everywhere.\n version: version_last.clone().or_else(|| version.clone()),\n version_first: version.or(version_last),"
}
],
"instrument": "`csift search '' @<id> --count-by version` on the clone beside the same census on its origin is only SUGGESTIVE - the clone's single head version could in principle be the origin's own tail version. The decisive instrument is a uuid JOIN across the two transcripts: intersect the top-level `uuid` sets and compare `version` (and every other top-level field) on the shared records. Counting rule = one per shared uuid; the claim holds iff origin.version != clone.version on all of them.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.3",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) csift list --max-count 0 --no-subagents --format json > list_all.json (enumerate every top-level transcript under ~/.claude/projects and read the clone_of / version_first / version_last fields); (2) csift search '' @<clone-id> --no-subagents --count-by version and csift search '' @<origin-id> --no-subagents --count-by version; (3) the decisive one - a python3 uuid JOIN over the two .jsonl files in the single project dir that holds the pair: load every non-blank line of each file, key by top-level `uuid`, intersect the key sets, and for each shared uuid compare every top-level field of the origin record against the clone record. Session ids and the project dir are redacted here; the commands are otherwise literal.",
"observed": "Corpus scope: 64 top-level transcripts across 14 project dirs; EXACTLY 1 row has a non-null `clone_of`. That row: version_first=2.1.252, version(last-seen)=2.1.257; its named origin row: version_first=2.1.220, version_last=2.1.252. csift census, clone: `1037 2.1.252` + `813 2.1.257` (1850 matched records, 2 version keys). csift census, origin: `8654 2.1.231`, `7219 2.1.220`, `2283 2.1.223`, `756 2.1.221`, `371 2.1.251`, `9 2.1.252` (19292 matched records, 6 version keys). uuid JOIN: clone = 3943 distinct uuids over 5941 non-blank lines, origin = 70729 over 91975, intersection = 558 uuids. On those 558: `version` is 2.1.251 in the origin and 2.1.252 in the clone for 558/558 - shared-with-SAME-version = 0. `timestamp` is byte-identical on 558/558. The other fields that differ on all 558 are `sessionId` (rewritten to the clone's own id), `slug` (present in the origin, ABSENT in the clone) and `sessionKind` (null in the origin, \"bg\" in the clone); `promptId` differs on 89, `cwd` on 5, `parentUuid` on 2, `message` on 2. The clone's first TIMESTAMPED record is `type:\"system\" subtype:\"compact_boundary\"` at file line 28, version 2.1.252, ts 2026-08-31T11:41:41.866Z, compactMetadata.trigger=auto, preTokens=976521, postTokens=165848; the SAME uuid in the origin carries version 2.1.251, a slug, and sessionKind null. The clone file's birthtime is 2026-09-01 13:00:42 local, i.e. AFTER the earliest record timestamp it carries (2026-08-31T11:35:10.097Z) - timestamps predate the file. Control that the writer stamps its own build: `csift search '' --count-by version --no-subagents --since 6h` returns `1229 2.1.258` as the top key, and 2.1.258 is the build in Claude Code's versions directory (~/.local/share/claude/versions/).",
"rule": "Clone enumeration: one row per top-level transcript from `csift list --format json` (kind==\"session\"), counted as a clone iff `clone_of` is non-null. Version census: one count per RECORD keyed by that record's top-level `version` (csift's --count-by version counts matched records; the empty pattern makes it a whole-scope census, so it sees only role-bearing candidate records - 1850 of the clone's 5941 lines). uuid join: one count per top-level `uuid` present in BOTH files; a uuid is 'version-rewritten' iff origin.version != clone.version. All-line version census on the clone (every non-blank line, not just candidates): 2161 x 2.1.252, 1782 x 2.1.257, 1998 with no version field.",
"note": "Code site confirmed: the 5-line snippet is present VERBATIM in src/session/summarize.rs at lines 134-138 (byte-exact substring test), so no code correction was needed. Coverage caveat a stranger should know: the corpus contains exactly ONE clone (1 of 64 top-level transcripts), and it was minted while Claude Code was on 2.1.252 - so the rewrite mechanism is witnessed at 2.1.252, not re-witnessed at 2.1.258. What was independently confirmed at 2.1.258 is only the underlying premise that the writing build stamps its own `version` on every record (1229 records written in the last 6h all read 2.1.258). A 2.1.258-minted background fork would be needed to re-witness the rewrite at the current build; the prediction is that its copied records would read 2.1.258 while the same uuids in the origin read whatever build wrote them. Two supporting details surfaced by the join that the claim did not mention and that make the fork mechanically legible: the clone stamps `sessionKind:\"bg\"` on the copied records (the origin has sessionKind null on all 91975 of its lines), and `strings -n 6 <claude-versions-dir>/2.1.258 | rg sessionKind` shows 16 hits in the current build including the literal ` filtered from /resume: sessionKind=#` and a guard reading `G1(t,\"sessionKind\");return a===\"daemon\"||a===\"daemon-worker\"`, so sessionKind is still a live field at 2.1.258."
}
]
},
{
"id": "REC-042",
"area": "record-model",
"behavior": "A `Read` tool result echoes `toolUseResult` as `{type, file:{...}}`, and for the `text` variant the `file` object carries exactly five always-present keys - `filePath`, `content`, `startLine` (1-based), `numLines`, `totalLines` - plus one optional sixth, `truncatedByTokenCap`, which the schema alone declares `.optional()`.",
"depends": "csift `recover` reads those five keys to mint a read anchor or a windowed splice - a rename of any one silently degrades every Read into a zero-line partial and `recover --file` reports `no recoverable history` - and it never reads `truncatedByTokenCap`, so its full-versus-partial decision is blind to the one key that says the window was capped.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "55-58",
"snippet": " // ── (1a) Read result: toolUseResult.file = {filePath, content, startLine, …} ──\n if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {"
},
{
"path": "src/recover/carriers.rs",
"lines": "64-71",
"snippet": " let start_line = file\n .get(\"startLine\")\n .and_then(serde_json::Value::as_u64)\n .unwrap_or(1) as usize;\n let total_lines = file\n .get(\"totalLines\")\n .and_then(serde_json::Value::as_u64)\n .map(|n| n as usize);"
},
{
"path": "src/recover/carriers.rs",
"lines": "179-181",
"snippet": " let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;"
}
],
"instrument": "Parse every jsonl line under ~/.claude/projects whose `toolUseResult.file` is an object and tally `tuple(sorted(file))`. Counting rule: one tally per echo. Measured 2026-09-02 over the 120 most-recently-modified top-level transcripts: the modal tuple is `('content','filePath','numLines','startLine','totalLines')` with 5600 records, the next is the same tuple plus `truncatedByTokenCap` (99). A second sweep over 20 sampled transcripts of 500 KB-25 MB: 433 five-key, 30 six-key, 29 `('filePath',)` (the `file_unchanged` variant), 1 `('count','filePath','originalSize','outputDir')` (the `parts` variant). Schema authority: the Read-result zod union in <the Claude Code 2.1.258 binary> declares only `truncatedByTokenCap` optional.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.7 (the recover event-source table, `full Read` / `windowed Read` rows); measured now"
},
"first_seen_claude_code": "2.1.150",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc258.strings; rg -F truncatedByTokenCap cc258.strings # then print the 400 chars before each hit\nAND the corpus key-tuple tally: python3 - <<'EOF'\nimport json,os,collections,re\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n if not f.endswith('.jsonl'): continue\n sc='sub' if os.sep+'subagents'+os.sep in dp else 'top'\n for raw in open(os.path.join(dp,f),errors='replace'):\n if 'toolUseResult' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n t=r.get('toolUseResult')\n if not isinstance(t,dict): continue\n fo=t.get('file')\n if not isinstance(fo,dict): continue\n c[(sc,t.get('type'),tuple(sorted(fo)))]+=1\nprint(c)\nEOF",
"observed": "Binary (2.1.258), the Read-result zod union, verbatim: 'c({type:x(\"text\"),file:c({filePath:i().describe(\"The path to the file that was read\"),content:i().describe(\"The content of the file\"),numLines:A().describe(\"Number of lines in the returned content\"),startLine:A().describe(\"The starting line number\"),totalLines:A().describe(\"Total number of lines in the file\"),truncatedByTokenCap:M().optional().describe(\"True when a whole-file read was auto-paginated because it exceeded the token cap (the content is a partial first page). A programmatic signal for internal consumers; survives output reconstruction (unlike the render-time banner).\")})' - the five keys carry no .optional(), only truncatedByTokenCap does. Corpus tally 2026-09-02: file key tuple ('content','filePath','numLines','startLine','totalLines') 8434 records and the same tuple plus 'truncatedByTokenCap' 174 records = every one of the 8608 `text` echoes; no `text` echo carries any other tuple and none is missing a key. Top-level transcripts alone read 5601 five-key + 99 six-key (the claim's 5600/99, +1 as the corpus grew). Other variants in the same tally: ('filePath',) 120 (file_unchanged), ('count','filePath','originalSize','outputDir') 26 (parts), ('base64','dimensions','originalSize','type') 435 and ('base64','originalSize','type') 9 (image), ('base64','filePath','originalSize') 4 (pdf).",
"rule": "One observation per jsonl record under ~/.claude/projects whose parsed toolUseResult is a dict with a dict `file`; key = (top-level vs subagent transcript, toolUseResult.type, sorted tuple of file's keys). Scope = every *.jsonl under the projects root (86 top-level + 7736 subagent transcripts, 1.31M lines).",
"note": "Both instruments agree with the claim as written. The schema is unchanged at 2.1.258 and every text echo on disk carries the five keys. Counts are live-corpus totals and drift upward as sessions run; the claim's top-level-only numbers reproduced to +1. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:55-58, :64-71 and :179-181."
}
]
},
{
"id": "REC-043",
"area": "record-model",
"behavior": "A `Read` result's `toolUseResult` is a `type`-discriminated union with exactly six variants - `text`, `image`, `notebook`, `pdf`, `parts` and `file_unchanged` - each carrying its own differently-shaped `file` sub-object (`image` gives `{base64, originalSize, type}` plus an OPTIONAL `dimensions`; `parts` gives `{count, filePath, originalSize, outputDir}` and may carry the sibling keys `firstPage` and `pages`, `pages` being transient and absent on the persisted record).",
"depends": "csift's `recover` Read arm keys only on the presence of a `file` object with a matching `filePath`, never on `type`, so every non-`text` variant reaches the same code path and only the `total > 0` gate in `push_read_event` stops a contentless variant from minting a bogus empty anchor; csift's own doc comment enumerates just five values (`create`, `update`, `file_unchanged`, `text`, `image`) and is the incomplete map a future contributor reads.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "55-58",
"snippet": " // ── (1a) Read result: toolUseResult.file = {filePath, content, startLine, …} ──\n if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {"
},
{
"path": "src/recover/carriers.rs",
"lines": "178-181",
"snippet": " let lines: Vec<String> = split_lines(content);\n let observed = num_lines.unwrap_or(lines.len());\n let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n let is_full = start_line == 1 && observed >= total && total > 0;"
},
{
"path": "src/model/mutation.rs",
"lines": "181-184",
"snippet": " /// `toolUseResult.type` ∈ {`create`, `update`, `file_unchanged`, `text`, `image`};\n /// only `create` ⇒ `is_create = true`, everything else ⇒ `false`. When there is no\n /// `toolUseResult`, it is not an object, or it has no `filePath`, the result is\n /// empty (defensive arms, each tested)."
}
],
"instrument": "Tally `Counter(tur['type'])` over every jsonl record whose `toolUseResult.file` is an object. Counting rule: one tally per echo. Measured 2026-09-02 over every *.jsonl under ~/.claude/projects: text 8608, image 444, file_unchanged 120, parts 26, pdf 4 (9202 file-bearing echoes); `notebook` is declared by the schema but occurs 0 times in this corpus. Schema authority: the Read-result zod union in the Claude Code 2.1.258 binary carries all six discriminants.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured now; src/recover/carriers.rs:55-89 (the Read arm keys on `file`, not on `type`); src/model/mutation.rs:181-184 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import sys;l=open('cc258.strings',errors='replace').read().split('\\n');s=[x for x in l if 'truncatedByTokenCap' in x][2];i=s.find('vi(\\\"type\\\",[');print(s[i:i+3400])\" # cc258.strings = strings -n 6 ~/.local/share/claude/versions/2.1.258\nAND the corpus variant tally: python3 - <<'EOF'\nimport json,os,collections,re\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n if not f.endswith('.jsonl'): continue\n sc='sub' if os.sep+'subagents'+os.sep in dp else 'top'\n for raw in open(os.path.join(dp,f),errors='replace'):\n if 'toolUseResult' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n t=r.get('toolUseResult')\n if not isinstance(t,dict): continue\n fo=t.get('file')\n if not isinstance(fo,dict): continue\n c[(sc,t.get('type'),tuple(sorted(fo)))]+=1\nprint(c)\nEOF",
"observed": "Binary (2.1.258): one discriminated union, in order - c({type:x(\"text\"),file:c({filePath,content,numLines,startLine,totalLines,truncatedByTokenCap optional}),artifactRead optional}), c({type:x(\"image\"),file:c({base64,type,originalSize,dimensions:c({originalWidth,originalHeight,displayWidth,displayHeight}).optional()})}), c({type:x(\"notebook\"),file:c({filePath,cells})}), c({type:x(\"pdf\"),file:c({filePath,base64,originalSize})}), c({type:x(\"parts\"),file:c({filePath,originalSize,count,outputDir}),firstPage optional,pages optional}), c({type:x(\"file_unchanged\"),file:c({filePath}),source:x(\"seeded\").optional()}) - exactly six variants. Corpus 2026-09-02: text 8608, image 444, file_unchanged 120, parts 26, pdf 4 (9202 file-bearing echoes); notebook 0. Image file shapes split ('base64','dimensions','originalSize','type') 435 vs ('base64','originalSize','type') 9. Top-level toolUseResult key tuples: ('file','type') 9196, ('file','source','type') 4, ('file','firstPage','type') 2.",
"rule": "One observation per record whose toolUseResult is a dict with a dict `file`, keyed by toolUseResult.type and by the sorted key tuples of both the toolUseResult and its `file`. Same whole-projects-root scope as REC-042.",
"note": "The six-variant union is confirmed verbatim in the 2.1.258 binary and the five observable discriminants reproduce on disk (counts +3 on `text` as the corpus grew). Two wording corrections: `dimensions` is declared .optional() and is absent on 9 of 444 image echoes, so the image `file` shape is not fixed at four keys; and `parts` carries two further siblings of `file` (`firstPage` seen on 2 records, `pages` documented as never persisted). `notebook` cannot be exercised here - no .ipynb has been read on this machine - so its on-disk shape stays schema-only. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:55-58, :178-181 and src/model/mutation.rs:181-184."
}
]
},
{
"id": "REC-044",
"area": "record-model",
"behavior": "The `Read` echo's `toolUseResult.file.content` is RAW file text: no line-number gutter, no `<system-reminder>` wrapper and no ellipsis marker, even when the read was windowed or token-cap truncated. The `NNN\\t<text>` gutter exists only in the model-facing `tool_result` block text.",
"depends": "csift `recover` splices `file.content` into the reconstruction buffer verbatim with no gutter strip, so `recover --file X > X` would emit gutter-prefixed garbage if the echo ever carried the decorated form.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "59-63",
"snippet": " let content = file\n .get(\"content\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default()\n .to_string();"
},
{
"path": "src/recover/diff.rs",
"lines": "165-170",
"snippet": "/// Split content into lines WITHOUT a trailing empty element for a final newline (so a\n/// file `\"a\\nb\\n\"` is `[\"a\",\"b\"]`, matching how CC numbers lines).\npub(crate) fn split_lines(content: &str) -> Vec<String> {\n if content.is_empty() {\n return Vec::new();\n }"
},
{
"path": "src/recover/carriers.rs",
"lines": "59-64",
"snippet": " let content = file\n .get(\"content\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default()\n .to_string();\n let start_line = file"
}
],
"instrument": "For every `text` Read echo carrying a string `file.content`, classify its FIRST line by whether it matches a digits-then-tab or digits-then-arrow gutter. Counting rule: one tally per echo. Measured 2026-09-02 over the 120 most-recently-modified top-level transcripts: none 5699, tab 0, arrow 0; a corpus-wide sweep read 0 gutter-shaped of 8608.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.7 (`content is RAW, no gutter`); measured now"
},
"first_seen_claude_code": "2.1.150",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 - <<'EOF'\nimport json,os,collections,re\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n if not f.endswith('.jsonl'): continue\n for raw in open(os.path.join(dp,f),errors='replace'):\n if 'toolUseResult' not in raw: continue\n try: t=json.loads(raw).get('toolUseResult')\n except Exception: continue\n if not isinstance(t,dict) or t.get('type')!='text': continue\n ct=(t.get('file') or {}).get('content')\n if not isinstance(ct,str): continue\n c['gutter' if re.match(r'^\\s*\\d+[\\t\\u2192]',ct[:60]) else 'raw']+=1\n c['sysrem'] += ct.startswith('<system-reminder>')\n c['trunc_banner'] += '[Truncated: PARTIAL view' in ct[:200]\nprint(c)\nEOF",
"observed": "{'raw': 8609} - zero gutter-shaped first lines, zero contents opening `<system-reminder>`, zero contents carrying the `[Truncated: PARTIAL view` banner; the 122 echoes stamped version 2.1.258 are all raw. Firsthand cross-check at 2.1.258: a Read of a purpose-built 12-line file whose model-facing render came back as `1\\tzeta01` ... `13\\t` - the gutter lives only on the render side.",
"rule": "One observation per `text` Read echo carrying a string `file.content`; classify the first 60 characters by the regex `^\\s*\\d+[\\t\\u2192]` (tab or arrow gutter) versus raw. Whole projects root.",
"note": "8609 of 8609 text echoes carry undecorated file text; the count is one above the claim's 8608 only because the corpus grew during the sweep. Neither the render-time truncation banner nor a system-reminder ever leaks into the structured `file.content`. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:59-63 and :59-64, src/recover/diff.rs:165-170."
}
]
},
{
"id": "REC-045",
"area": "record-model",
"behavior": "The MODEL-facing side of a Read - the `tool_result` block's `content` string, the only copy a subagent lane keeps - IS gutter-rendered as `<line-number><TAB><text>` per line, and 265 of 8608 instead carry a bare `<system-reminder>` warning such as `Warning: the file exists but the contents are empty.` or `the file exists but is shorter than the provided offset`.",
"depends": "csift never parses the rendered side for content, so it loses nothing on a top-level session - and it loses nothing on a built-in Task or teammate subagent transcript either, since those DO carry the structured echo (3129 file-bearing echoes corpus-wide, 18 at CC 2.1.258). It is the WORKFLOW subagent transcript (subagents/workflows/**) that carries no `toolUseResult` at all - 0 file-bearing echoes corpus-wide - so there the gutter-rendered `tool_result` is the ONLY surviving copy of the file text and `recover` cannot use it.",
"code": [
{
"path": "src/recover/diff.rs",
"lines": "185-188",
"snippet": "/// Strip a leading line-number gutter from each line of a cat -n style snippet. Handles\n/// BOTH the TAB gutter (`\\d+\\t<text>`, what current CC Read content uses) and the arrow\n/// gutter (`\\d+→<text>`, an older form). Returns `(file_line_no, text)` pairs; a line\n/// with no recognizable gutter is skipped (we never fabricate a number)."
}
],
"instrument": "Corpus: for records carrying both a `toolUseResult.file` and a string `tool_result` block, test the render's first line against `^\\s*\\d+\\t`; one observation per record. Measured now: 8343 gutter-rendered, 265 other (all `<system-reminder>` warnings).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'EOF'\nimport json,os,collections,re\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n if not f.endswith('.jsonl'): continue\n shape='top' if os.sep+'subagents'+os.sep not in dp else ('workflow' if os.sep+'workflows'+os.sep in dp else 'builtin')\n for raw in open(os.path.join(dp,f),errors='replace'):\n if 'toolUseResult' not in raw: continue\n try: r=json.loads(raw)\n except Exception: continue\n t=r.get('toolUseResult')\n if not isinstance(t,dict) or not isinstance(t.get('file'),dict): continue\n c[('echo',shape)]+=1\n if t.get('type')!='text': continue\n txt=None\n for b in (r.get('message') or {}).get('content') or []:\n if isinstance(b,dict) and b.get('type')=='tool_result' and isinstance(b.get('content'),str): txt=b['content']\n if txt is None: continue\n if re.match(r'^\\s*\\d+\\t',txt.split('\\n',1)[0]): c['gutter']+=1\n else: c['sysrem' if txt.startswith('<system-reminder>') else 'other']+=1\nprint(c)\nEOF",
"observed": "gutter 8343, sysrem 265, other 0 - an exact reproduction of the claim's 8343/265 of 8608. The 265 break down as `<system-reminder>Warning: the file exists but the contents are empty.` 100, `...is shorter than the provided offset` 27, `<system-reminder>[Truncated: PARTIAL view - showing lines 1-N of M ...` and `This memory is N days old` for the rest. Structured-echo census by transcript shape: top 6074, built-in-Task/teammate subagent 3129 (18 of them stamped 2.1.258), WORKFLOW subagent 0. Firsthand at 2.1.258: a Read issued inside a workflow-subagent lane produced a tool_result record whose render was `1\\tzeta01` ... `13\\t` and which carried no `toolUseResult` field at all (that transcript: 87 records, 0 with toolUseResult).",
"rule": "One observation per record that carries BOTH a dict `toolUseResult.file` of type `text` AND a string `tool_result` block; test the render's first line against `^\\s*\\d+\\t`, else bucket by whether it opens `<system-reminder>`. The shape census counts one observation per file-bearing echo, keyed by transcript location (top-level / subagents/agent-*.jsonl / subagents/workflows/**).",
"note": "The behavior claim reproduces exactly (8343 gutter / 265 system-reminder of 8608). The refinement is in the depends clause: 'a subagent transcript has no structured echo' is true only of workflow-subagent transcripts. Built-in Task/teammate subagent transcripts carry structured echoes at every CC version in this corpus including 2.1.258, so `recover` retains full fidelity there; the blind spot is narrower than stated but real - verified firsthand by reading two probe files from inside a workflow subagent lane and finding zero toolUseResult fields in that transcript. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/diff.rs:185-188."
}
]
},
{
"id": "REC-046",
"area": "record-model",
"behavior": "A re-Read of a file unchanged since the last Read echoes `{type:\"file_unchanged\", file:{filePath}}` - the `file` object carries ONLY `filePath`, no `content`, no `startLine`, no `numLines`, no `totalLines` - plus an optional `source:\"seeded\"` when the dedup matched a startup-seeded entry (CLAUDE.md / nested memory) rather than a prior Read result. The model-facing tool_result text depends on which of the two it was: without `source` it opens `Wasted call - file unchanged since your last Read. Refer to that earlier tool_result instead.` (the dash is an em dash), while a `source:\"seeded\"` record instead opens `<system-reminder>This file is already in your context (see \"Contents of ...\") and has not changed on disk.`",
"depends": "csift's `recover` Read arm matches this record on `file.filePath` and then defaults `content` to the empty string and `startLine` to 1, so the echo would become a zero-line snapshot; the `total > 0` clause of `is_full` demotes it to an empty `PartialRead`, so a `file_unchanged` echo neither anchors nor corrupts the buffer.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "181-188",
"snippet": " let is_full = start_line == 1 && observed >= total && total > 0;\n if is_full {\n events.push(FileEvent {\n line_no,\n turn_index,\n timestamp_utc: ts.clone(),\n kind: EventKind::FullSnapshot {\n content: content.to_string(),"
},
{
"path": "src/recover/carriers.rs",
"lines": "56-63",
"snippet": " if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {\n let content = file\n .get(\"content\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default()\n .to_string();"
}
],
"instrument": "Tally `tuple(sorted(tur))`, `tuple(sorted(tur['file']))` and (tur.get('source'), tool_result render prefix) for every `toolUseResult` whose `type` is `file_unchanged`. Counting rule: one tally per record. Measured 2026-09-02 over every *.jsonl under ~/.claude/projects: 120 records, `('filePath',)` on all 120 file objects and zero carrying `content`; 116 keyed ('file','type') opening the Wasted-call text, 4 keyed ('file','source','type') with source=='seeded' opening the already-in-your-context reminder; observed on Claude Code 2.1.156 through 2.1.231. Binary cross-check: `rg -c -F 'file unchanged since your last Read'` over the 2.1.258 strings returns 1.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": "2.1.156",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -c -F 'file unchanged since your last Read' cc258.strings # cc258.strings = strings -n 6 ~/.local/share/claude/versions/2.1.258\nAND: the corpus sweep of REC-045's shape, restricted to toolUseResult.type=='file_unchanged', tallying (tur['source'], first 28 chars of the tool_result render) and tuple(sorted(tur)) / tuple(sorted(tur['file'])).",
"observed": "Binary count 1, and the surrounding definitions verbatim: q=\"File unchanged since last read. The content from the earlier Read tool_result in this conversation is still current \\u2014 refer to that instead of re-reading.\", A=\"Wasted call \\u2014 file unchanged since your last Read. Refer to that earlier tool_result instead.\", R=\"<system-reminder>This file is already in your context\". Schema: c({type:x(\"file_unchanged\"),file:c({filePath:i().describe(\"The path to the file\")}),source:x(\"seeded\").optional().describe(\"Set when the dedup matched a startup-seeded entry (CLAUDE.md / nested memory) rather than a prior Read tool_result\")}). Corpus: 120 file_unchanged records - file keys ('filePath',) on all 120; toolUseResult keys ('file','type') 116 and ('file','source','type') 4 with source=='seeded' on all 4; render pairing is exact - the 116 sourceless records open 'Wasted call \\u2014 file unchanged since your last Read. Refer to that earlier tool_result instead.' and the 4 seeded records open '<system-reminder>This file is already in your context (see \"Contents of ...\")'. Versions carrying the variant: 2.1.156 through 2.1.231, none newer in this corpus.",
"rule": "One observation per record whose toolUseResult.type == 'file_unchanged', keyed by the sorted key tuples and by (source, render prefix). Whole projects root.",
"note": "Shape, key tuples and the seeded flag reproduce exactly (116 + 4 = 120). Two corrections: the separator in the render is an em dash and the sentence continues 'Refer to that earlier tool_result instead.', and the seeded variant does NOT use that opener - it emits the '<system-reminder>This file is already in your context' form instead, a 1:1 split in the data. The variant is still declared and its string still present in 2.1.258, but no record newer than 2.1.231 in this corpus exercised it, so the on-disk-at-2.1.258 shape is unobserved here. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:56-63 and :181-188."
}
]
},
{
"id": "REC-047",
"area": "record-model",
"behavior": "A `Read` clipped by the token cap sets `toolUseResult.file.truncatedByTokenCap:true` and, in every observed case, keeps `startLine:1` while making `numLines` strictly less than `totalLines`.",
"depends": "csift `recover` does not read `truncatedByTokenCap`; it relies entirely on `numLines >= totalLines` to decide a full-file anchor, so a token-capped Read staying `numLines < totalLines` is what keeps a partial file from being replayed as ground truth.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "68-75",
"snippet": " let total_lines = file\n .get(\"totalLines\")\n .and_then(serde_json::Value::as_u64)\n .map(|n| n as usize);\n let num_lines = file\n .get(\"numLines\")\n .and_then(serde_json::Value::as_u64)\n .map(|n| n as usize);"
},
{
"path": "src/recover/types.rs",
"lines": "17-23",
"snippet": " /// Full ground-truth content (an anchor): a Write result, a full Read\n /// (`startLine==1 && numLines==totalLines`), or a `file` attachment.\n FullSnapshot {\n content: String,\n total_lines: usize,\n source: SnapSource,\n },"
}
],
"instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if 'truncatedByTokenCap' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n fo=t.get('file') if isinstance(t,dict) else None\n if isinstance(fo,dict) and fo.get('truncatedByTokenCap'): k[(fo.get('startLine'),fo.get('numLines')==fo.get('totalLines'))]+=1\nprint(k)\nEOF\nCounting rule: one tally per Read echo with `file.truncatedByTokenCap` truthy, keyed by `(startLine, numLines==totalLines)`. Measured 2026-09-02: `(1, False)` 99 records, no other key.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 - <<'EOF'\nimport json,os,collections\nR=os.path.expanduser('~/.claude/projects'); k=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n if not f.endswith('.jsonl'): continue\n sc='sub' if os.sep+'subagents'+os.sep in dp else 'top'\n for line in open(os.path.join(dp,f),errors='replace'):\n if 'truncatedByTokenCap' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n fo=t.get('file') if isinstance(t,dict) else None\n if isinstance(fo,dict) and fo.get('truncatedByTokenCap'):\n k[(sc,t.get('type'),fo.get('startLine'),fo.get('numLines')==fo.get('totalLines'))]+=1\nprint(k)\nEOF",
"observed": "Counter({('top','text',1,False): 99, ('sub','text',1,False): 75}) - 174 token-capped echoes, every one startLine 1, every one numLines != totalLines (and numLines < totalLines in all 174, since they are also the intersection of the partial bucket). The claim's 99 is exactly the top-level-transcript half. Binary corroboration (2.1.258): truncatedByTokenCap is described as 'True when a whole-file read was auto-paginated because it exceeded the token cap (the content is a partial first page)', and the construction clamps only on that path - 'startLine:Ue!==void 0?Math.max(1,f):f' with '...Ue!==void 0&&{truncatedByTokenCap:!0}' in the same object literal.",
"rule": "One observation per Read echo whose `file.truncatedByTokenCap` is truthy, keyed by (transcript scope, type, startLine, numLines==totalLines). Whole projects root.",
"note": "Reproduces with no exception in 174 records. The binary explains why: the cap fires only on a WHOLE-file read (hence startLine 1) and the emitted content is a partial first page (hence numLines < totalLines), so csift's numLines>=totalLines full-anchor test cannot be fooled by a token-capped read even though it never reads the flag. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:68-75 and src/recover/types.rs:17-23."
}
]
},
{
"id": "REC-048",
"area": "record-model",
"behavior": "`startLine` is echoed back verbatim from the caller's `offset` when the read was not truncated, so it can be `0` - observed on two Read echoes at Claude Code 2.1.211 with `startLine:0, numLines:55, totalLines:601`.",
"depends": "csift's `is_full` demands `start_line == 1` exactly, so a `startLine:0` whole-file read is demoted to a partial (the safe direction) and `PartialRead` clamps the splice base with `start_line.max(1)`; a future rule keying on `startLine <= 1` would flip that safety.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "64-67",
"snippet": " let start_line = file\n .get(\"startLine\")\n .and_then(serde_json::Value::as_u64)\n .unwrap_or(1) as usize;"
},
{
"path": "src/recover/carriers.rs",
"lines": "198-202",
"snippet": " kind: EventKind::PartialRead {\n start_line: start_line.max(1),\n lines,\n total_lines: total,\n },"
}
],
"instrument": "Corpus: count echoes whose `toolUseResult.file.startLine` is exactly `0`, one observation per echo. Measured now over all of ~/.claude/projects: 2 of 9199, both at record `version` 2.1.211. Binary: the construction reads `startLine:Ue!==void 0?Math.max(1,f):f`, so the clamp applies only on the truncated path.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 - <<'EOF'\nimport json,os,collections\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if 'toolUseResult' not in line: continue\n try: r=json.loads(line)\n except Exception: continue\n t=r.get('toolUseResult'); fo=t.get('file') if isinstance(t,dict) else None\n if isinstance(fo,dict) and fo.get('startLine')==0: c[r.get('version')]+=1\nprint(c)\nEOF\nAND: rg -F 'startLine:Ue' cc258.strings",
"observed": "Counter({'2.1.211': 2}) - exactly 2 echoes with startLine 0 among the 9202 file-bearing echoes, both stamped version 2.1.211, both top-level. Binary 2.1.258, the emit site verbatim: 'let ht={type:\"text\",file:{filePath:n,content:De,numLines:Ne,startLine:Ue!==void 0?Math.max(1,f):f,totalLines:ke,...Ue!==void 0&&{truncatedByTokenCap:!0}},...pt&&{artifactRead:pt}}' - the Math.max(1,...) clamp is applied only when Ue (the token-cap marker) is set, so the untruncated path echoes the caller's offset unclamped. Distribution of startLine over the same echoes: 0 -> 2, 1 -> 4450, >1 -> 4156.",
"rule": "One observation per Read echo whose `toolUseResult.file.startLine` is exactly 0, keyed by the record's `version`. Whole projects root.",
"note": "Both halves confirmed: the 2 zero-offset records are still on disk at exactly the stated version, and the 2.1.258 binary still contains the conditional clamp that lets it happen, so this is live behavior rather than a fossil. csift's `start_line == 1` full-anchor test and its `start_line.max(1)` splice base both remain verbatim at src/recover/carriers.rs:181 and :199. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:64-67 and :198-202."
}
]
},
{
"id": "REC-049",
"area": "record-model",
"behavior": "A `text` Read result may carry a second sibling key beside `file` - `artifactRead:{slug, ver}` - set when the read completed a saved artifact source file, so such a record's `toolUseResult` has three top-level keys (`type`, `file`, `artifactRead`) rather than the usual two.",
"depends": "csift ignores it, which is correct today, but any code that assumes `toolUseResult` for a Read has exactly the keys `type` and `file` will mis-handle those records.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "55-58",
"snippet": " // ── (1a) Read result: toolUseResult.file = {filePath, content, startLine, …} ──\n if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {"
}
],
"instrument": "Structural census over every *.jsonl under ~/.claude/projects: count records whose parsed `toolUseResult` has `artifactRead` as a KEY. Measured 2026-09-02: 0 of 9202 file-bearing echoes; the toolUseResult key tuples observed are ('file','type') 9196, ('file','source','type') 4, ('file','firstPage','type') 2. A raw `grep artifactRead` over the corpus is NOT this measurement - it returns prose mentions from sessions that discussed the key. Schema authority: the `artifactRead:c({slug:i(),ver:i()}).optional()` member of the `text` variant in the Read-result zod union of the Claude Code 2.1.258 binary, plus the emit site `...pt&&{artifactRead:pt}`.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "rg -c -F artifactRead cc258.strings # cc258.strings = strings -n 6 ~/.local/share/claude/versions/2.1.258\nAND the corpus sweep: for every record whose toolUseResult is a dict, count separately (a) records where 'artifactRead' is a KEY of toolUseResult and (b) records where the raw line merely CONTAINS the string 'artifactRead'.",
"observed": "Binary: 33 strings match; the schema member verbatim is 'artifactRead:c({slug:i(),ver:i()}).optional().describe(\"Set when this Read completed a saved Artifact source file: the Artifact and the version of it that now counts as viewed.\")', declared as a sibling of `file` INSIDE the `text` variant, and the emit site spreads it conditionally: '...pt&&{artifactRead:pt}'. Corpus: 0 records carry `artifactRead` as a key of `toolUseResult` (the toolUseResult key-tuple census over 9202 file-bearing echoes reads only ('file','type') 9196, ('file','source','type') 4, ('file','firstPage','type') 2); the only 3 lines that contain both 'toolUseResult' and 'artifactRead' are prose mentions inside transcripts that were discussing this key.",
"rule": "Key-presence census: one observation per record whose toolUseResult is a dict, testing `'artifactRead' in toolUseResult` (structural) versus `'artifactRead' in raw_line` (textual). Whole projects root.",
"note": "The mechanism is confirmed at the binary level in 2.1.258 (schema member plus conditional spread at the emit site), so the behavior claim stands. The instrument does not: a textual grep for the key counts sessions that merely TALK about it, which is how the claim arrived at 10 lines. The structural census finds zero real occurrences in this corpus - no artifact source file has been read here - so the on-disk shape of such a record is unobserved and only the schema fixes it. 'SEVENTH sibling key' also misdescribes the nesting: it is the second sibling of `type`/`file`, not the seventh key of `file`. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:55-58."
}
]
},
{
"id": "REC-050",
"area": "record-model",
"behavior": "`totalLines` and `numLines` are SEPARATOR counts, so a newline-terminated file reports one more than its content lines (a 12-content-line file ending in `\\n` reports 13, and its model-facing render carries an empty numbered line 13). This is not a minority effect: every newline-terminated echo counts the phantom line, which is a majority of all Read echoes.",
"depends": "csift normalises the phantom away in `SparseBuffer::normalize_total` once a full anchor has confirmed the trailing newline; without it a fully-recovered file is mis-reported as missing its last line and `recover` restore hard-fails as partial.",
"code": [
{
"path": "src/recover/buffer.rs",
"lines": "59-65",
"snippet": " pub(crate) fn normalize_total(&self, total_lines: usize) -> usize {\n if self.content_ends_with_newline {\n total_lines.saturating_sub(1)\n } else {\n total_lines\n }\n }"
}
],
"instrument": "Two rules. (1) Controlled, at CC 2.1.258: Read a 12-line file ending in a newline and its twin without the final newline; the renders come back with 13 numbered lines (last one empty) and 12 respectively. (2) Corpus, over every *.jsonl under ~/.claude/projects: for full reads (startLine==1, numLines==totalLines) whose content ends in a newline - 3481 echoes - 3480 report totalLines == content.count('\\n')+1, one more than the content's own line count. Of all 4512 echoes with newline-terminated content, 4512 have numLines == count('\\n')+1 and 4508 have numLines == len(splitlines())+1.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/recover/buffer.rs:19-25 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Controlled read at CC 2.1.258: write a 12-line file ending in a newline (probe_nlterm_12.txt, 84 bytes, 12 newlines) and its byte-identical twin without the final newline (probe_nonl_12.txt, 83 bytes, 11 newlines), Read both, then read back the tool_result renders from the calling transcript.\nAND the corpus rule: for every `text` echo with startLine==1 and numLines==totalLines (a full read), tally (content.endswith('\\n'), totalLines==content.count('\\n')+1, totalLines==len(content.splitlines())).\nAND the claim's own rule re-run: for every `text` echo compare numLines against content.count('\\n')+1.",
"observed": "Controlled: the newline-terminated 12-line file rendered 13 numbered lines, last line the literal `13\\t` (empty); the non-terminated twin rendered 12, last line `12\\tzeta12`. Corpus full reads: newline-terminated 3481, of which 3480 report totalLines == content.count('\\n')+1 == splitlines+1 (one more than the file's own line count) and 1 does not; non-newline-terminated 265, where totalLines == count+1 == splitlines for 68 and the remainder differ only via splitlines' extra separators (\\r, \\x0b, \\x0c, U+2028). Re-running the claim's own numLines rule over all 8608 text echoes: 8415 equal, 1 where numLines is one MORE than count('\\n')+1, 4 one LESS, 188 other - the claim's 8290/126/189 does not reproduce. The sharper statement: of the 4512 echoes whose content ends in a newline, ALL 4512 have numLines == count('\\n')+1 and 4508 have numLines == splitlines+1, i.e. the phantom line is counted on essentially every newline-terminated echo, a MAJORITY of echoes (52%), not a minority.",
"rule": "Full-read rule: one observation per `text` echo with startLine==1 and numLines==totalLines, keyed by whether content ends in a newline and whether totalLines equals the separator count (count('\\n')+1) or the terminator count (len(splitlines())). Phantom rule: one observation per `text` echo with a string content and integer numLines, keyed by (endswith newline, numLines==count('\\n')+1, numLines==len(splitlines())). Whole projects root.",
"note": "The separator semantics that csift's SparseBuffer normalises away are confirmed twice over, including a live 2.1.258 read whose render numbered a 13th empty line for a 12-line file. What needed correction is the framing and the numbers: the claim's own numLines-versus-content rule re-runs to 8415/1/4/188 rather than 8290/126/189, and the phantom is the rule for newline-terminated files rather than 'a minority of echoes'. csift's code sites are verbatim: the SEPARATOR-count comment at src/recover/buffer.rs:19-25 and normalize_total at src/recover/buffer.rs:59-65. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/buffer.rs:59-65 (and the cited :19-25 comment)."
}
]
},
{
"id": "REC-051",
"area": "record-model",
"behavior": "Most reads in a real corpus are PARTIAL windows: 4812 of 8608 `file` echoes with both line counts (55.9%) report `numLines < totalLines`, and only 3.6% of those partials (174) are token-cap truncations - the rest are ordinary offset/limit windows.",
"depends": "csift's replay therefore spends most of its time on `PartialRead` splices rather than full anchors, which is why the gap accounting (`??? lines A..B unknown`) and not the anchor path is `recover`'s dominant correctness surface.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "193-198",
"snippet": " } else {\n events.push(FileEvent {\n line_no,\n turn_index,\n timestamp_utc: ts.clone(),\n kind: EventKind::PartialRead {"
}
],
"instrument": "Over every *.jsonl under ~/.claude/projects (no sampling), count echoes with integer `numLines` and `totalLines` where `numLines < totalLines`, one observation per echo. Measured 2026-09-02: 4812 partial of 8608 (55.9%); 174 of those 4812 carry `truncatedByTokenCap`, so token-capping explains 3.6% of the partials.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'EOF'\nimport json,os,collections\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n if not f.endswith('.jsonl'): continue\n for line in open(os.path.join(dp,f),errors='replace'):\n if 'toolUseResult' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n fo=t.get('file') if isinstance(t,dict) else None\n if not isinstance(fo,dict): continue\n nl,tl=fo.get('numLines'),fo.get('totalLines')\n if isinstance(nl,int) and isinstance(tl,int):\n c['lt' if nl<tl else ('eq' if nl==tl else 'gt')]+=1\n if nl<tl: c['capped' if fo.get('truncatedByTokenCap') else 'uncapped']+=1\nprint(c)\nEOF",
"observed": "lt 4812, eq 3796, gt 0 - 4812 of 8608 echoes (55.9%) are partial windows. Of those 4812 partials, 174 carry truncatedByTokenCap and 4638 do not, so token-capping explains 3.6% of the partials, not about 11%. Split by transcript: top-level 3548 partial / 2152 full, subagent 1264 partial / 1644 full.",
"rule": "One observation per Read echo with integer `numLines` and `totalLines`, bucketed numLines<totalLines / == / >; partials further split by whether `file.truncatedByTokenCap` is truthy. Whole projects root - every *.jsonl, no sampling.",
"note": "The headline - a majority of reads are windows, so replay lives on the PartialRead path - reproduces almost exactly at corpus scale (55.9% versus the claim's 56% on a 20-transcript sample). The token-cap share does not: it is 3.6% corpus-wide rather than about 11%, so the claim's sample over-represented capped reads by roughly 3x. Replacing the sample with the whole corpus also makes the number rerunnable without knowing which 20 transcripts were picked. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:193-198."
}
]
},
{
"id": "REC-052",
"area": "record-model",
"behavior": "The truncation banner is ALSO persisted as its own record: `type:\"attachment\"` with `attachment:{type:\"read_truncation_notice\", banner:\"<text>\", toolUseID:\"toolu_...\"}`, carrying a normal `uuid`/`timestamp`/`parentUuid`/`sessionId`/`version` and joining back to the Read by `toolUseID`.",
"depends": "csift classifies it under `harness.meta.attachment`, which is parsed only under `search --attachments` / `--count-by attachment` or an explicit `show` address - so a default scan cannot see that a read was capped, and `recover` ignores the record entirely.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "159-166",
"snippet": " // Any OTHER `type:\"attachment\"` record (edited_text_file, compact_file_reference,\n // file snapshots, …): harness sidecar payload - labeled `harness.meta.attachment`.\n // Only `search --attachments` / `--count-by attachment` (or an explicit `show`\n // address) ever parses these lines; the record never opens a turn.\n if self.attachment_payload_text().is_some() {\n push_unique(&mut out, Class::MetaAttachment);\n return out;\n }"
},
{
"path": "src/recover/carriers.rs",
"lines": "216-222",
"snippet": " let atype = att.get(\"type\").and_then(serde_json::Value::as_str);\n\n // (7a) edited_text_file → an external edit (hard boundary).\n if atype == Some(\"edited_text_file\") {\n let path = att\n .get(\"filename\")\n .or_else(|| att.get(\"filePath\"))"
}
],
"instrument": "Clean parsed count is now 184 (was 170) - the corpus is live and this class of record keeps accruing. The claim's second half (\"the same search without --attachments returns 0 ... 6 vs 0 on one project\") no longer discriminates on the csift project dir, because that corpus now contains sessions that quote the literal 'read_truncation_notice' in prose: measured there 30 with the gate vs 25 without. Run the contrast on a project whose sessions never discuss the mechanism: measured 40 vs 0 and 22 vs 0 on two such dirs.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": "2.1.206",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -o '.\\{200\\}read_truncation_notice.\\{260\\}' ; (2) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping records whose attachment.type == 'read_truncation_notice' and tallying sorted(attachment.keys()), the record type, and the presence of uuid/timestamp/parentUuid/sessionId/version; (3) same sweep joining each notice's attachment.toolUseID against the set of tool_use block ids with name=='Read' in the SAME file; (4) csift search 'read_truncation_notice' <one project-dir token> --attachments -t harness.meta.attachment -c versus the same command without --attachments, and versus -t harness.meta.attachment with no --attachments; (5) csift show <one transcript path> --line <N>",
"observed": "Binary emitter site, verbatim: we.push({message:Sn({type:\"read_truncation_notice\",banner:_o,toolUseID:n})}) . Corpus: 184 records with attachment.type=='read_truncation_notice'; the attachment key set is 'banner,toolUseID,type' on 184/184 (no other key set observed); the carrying record type is \"attachment\" on 184/184; uuid, timestamp, parentUuid, sessionId and version are all non-null on 184/184; banner is a string on 184/184. toolUseID join: 184/184 hit a Read tool_use id in the same transcript file, 0 misses. csift gate on two project dirs: 40 vs 0 and 22 vs 0 (with --attachments vs default scan); -t harness.meta.attachment WITHOUT --attachments returns 0 on all three project dirs tried. csift show <transcript> --line 23 renders it flag-free as '⚙ harness.meta.attachment L23 {\"type\":\"read_truncation_notice\",\"banner\":\"[Truncated: PARTIAL view — <path>: showing lines 1-261 of 478 total (38905 tokens, cap 25000). Call Read with offset=262 limit=261 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]\",\"toolUseID\":\"toolu_...\"}'. recover's attachment handler has exactly two arms (grep of src/recover/carriers.rs lines 210-300): '(7a) edited_text_file' and '(7b) a `file` attachment' - no read_truncation_notice arm.",
"rule": "One count per RECORD whose parsed attachment.type equals the literal 'read_truncation_notice' (prose mentions excluded by requiring the parsed field, not a raw grep). Join rule: one count per notice, hit iff its toolUseID is in the set of Read tool_use ids in the same file. Gate rule: csift -c prints matched exchanges, one project dir per run.",
"note": "Behavior holds in full and is now pinned tighter than the claim states: the attachment payload key set is exactly {banner, toolUseID, type} on all 184 records (no variant seen), and the toolUseID join to a Read tool_use in the same file is 184/184 with zero misses - the join key is not merely plausible, it is total on this corpus. Both csift code sites verified verbatim at the claimed lines: src/model/classify.rs:159-166 (the MetaAttachment arm, `if self.attachment_payload_text().is_some()` at line 163) and src/recover/carriers.rs:216-222. The 'recover ignores the record entirely' half of the depends is confirmed structurally, not just by absence: the attachment handler branches only on 'edited_text_file' and a 'file' attachment."
}
]
},
{
"id": "REC-053",
"area": "record-model",
"behavior": "The `read_truncation_notice` attachment is on Claude Code's short allow-list of attachment types that are explicitly persisted into a forked or async agent's transcript, alongside `session_context`, `date`, `instructions` and `remote_session_change`.",
"depends": "The notice does survive into subagent transcripts (155 of 184 live there) and csift reaches it only under search --attachments - both confirmed. But the stated reason is wrong: subagent transcripts are not a lane \"that has no toolUseResult echo\". 27152 records under a subagents/ path carry a non-null toolUseResult, among them 1951 Edit echoes and 841 Write echoes. So the notice is one truncation signal among the normal carriers there, not the only structured signal available in that lane.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "159-165",
"snippet": " // Any OTHER `type:\"attachment\"` record (edited_text_file, compact_file_reference,\n // file snapshots, …): harness sidecar payload - labeled `harness.meta.attachment`.\n // Only `search --attachments` / `--count-by attachment` (or an explicit `show`\n // address) ever parses these lines; the record never opens a turn.\n if self.attachment_payload_text().is_some() {\n push_unique(&mut out, Class::MetaAttachment);\n return out;"
}
],
"instrument": "Seek `In.attachment.type===\"read_truncation_notice\"||In.attachment.type===\"session_context\"` in a `strings -a` dump of the 2.1.258 binary; a second site in the forked-agent recorder gates persistence on `xt.type===\"attachment\"&&xt.attachment.type===\"read_truncation_notice\"`. Corpus confirmation: at least one `read_truncation_notice` record exists inside a `subagents/agent-*.jsonl` file under ~/.claude/projects.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": "2.1.206",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -o '.\\{200\\}read_truncation_notice.\\{260\\}' ; (2) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, splitting the read_truncation_notice records by whether the file path contains a '/subagents/' component; (3) the same sweep tallying every record with a non-null toolUseResult, split by the same '/subagents/' test, and separately counting Edit echoes (toolUseResult with an 'oldString' key) and Write echoes (toolUseResult.type in {create,update} with a 'content' key) per lane",
"observed": "Async-agent allow-list, verbatim from the binary: if(In.attachment.type===\"read_truncation_notice\"||In.attachment.type===\"session_context\"||In.attachment.type===\"date\"||In.attachment.type===\"instructions\"||In.attachment.type===\"remote_session_change\")await Kne([In],Cn,xp,r.storageV5).catch(fat),xp=In.uuid,yn?.add(In.uuid); Forked-agent recorder, verbatim: je&&(xt.type===\"assistant\"||xt.type===\"user\"||xt.type===\"progress\"||xt.type===\"attachment\"&&xt.attachment.type===\"read_truncation_notice\") ... await Kne([xt],je,Ge,we.storageV5) . Corpus: of the 184 read_truncation_notice records, 155 sit inside subagents/agent-*.jsonl and 29 in top-level transcripts. BUT subagent transcripts are NOT toolUseResult-free: 27152 records under a '/subagents/' path carry a non-null toolUseResult, including 1951 Edit echoes and 841 Write echoes (top-level: 59146 / 11421 / 2048).",
"rule": "One count per record. Lane test = the literal '/subagents/' appearing as a path component of the transcript file. Edit echo = toolUseResult dict containing the key 'oldString'; Write echo = toolUseResult dict whose 'type' is 'create' or 'update' and which has a 'content' key.",
"note": "The behavioral claim - that read_truncation_notice is on a five-member allow-list of attachment types explicitly persisted into forked/async agent transcripts, alongside session_context, date, instructions and remote_session_change - is confirmed verbatim at two independent binary sites, and the corpus agrees (155 notices inside subagent transcripts). Only the depends clause needed correcting. csift code site verified verbatim: src/model/classify.rs:159-165."
}
]
},
{
"id": "REC-054",
"area": "record-model",
"behavior": "The `edited_text_file` attachment's payload keys are exactly `type`, `filename` and `snippet` - never `filePath` or `content` - and an EMPTY `snippet` string is a real, emitted form.",
"depends": "csift's `filename`-then-`filePath` and `snippet`-then-`content` fallbacks are dead alternatives on current Claude Code, and the empty-snippet case is the degraded external-edit boundary csift already names in the boundary detail.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "219-224",
"snippet": " if atype == Some(\"edited_text_file\") {\n let path = att\n .get(\"filename\")\n .or_else(|| att.get(\"filePath\"))\n .and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {"
}
],
"instrument": "Corpus: count `tuple(sorted(attachment.keys()))` for every record whose `attachment.type` is `edited_text_file`, one observation per record. Measured now over all of ~/.claude/projects: 1339 records, all with the key set `('filename','snippet','type')`; 85 of the 1339 carry an empty `snippet` (6.3%).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping records whose attachment.type == 'edited_text_file' and tallying ','.join(sorted(attachment.keys())) plus whether attachment['snippet'] == ''",
"observed": "1339 edited_text_file records corpus-wide. Key set: 'filename,snippet,type' on 1339/1339 - a single key set, no record carries 'filePath' and none carries 'content'. snippet is a string on 1339/1339 and is the empty string on 85 of them (6.3%).",
"rule": "One observation per record whose parsed attachment.type equals 'edited_text_file'; buckets are the sorted key tuple.",
"note": "Reproduces the claim exactly, to the record: 1339 records, one key set, 85 empty snippets. So csift's `.get(\"filename\").or_else(|| att.get(\"filePath\"))` fallback is a dead alternative on this corpus - 0 of 1339 records would take it - and the same holds for a `content` fallback. Code site verified verbatim at src/recover/carriers.rs:219-224. The empty-snippet form is real and not rare enough to ignore."
}
]
},
{
"id": "REC-055",
"area": "record-model",
"behavior": "A `Write` tool result echoes toolUseResult as {\"type\":\"create\"|\"update\",\"filePath\",\"content\",\"structuredPatch\",\"originalFile\",\"userModified\"} (plus an optional gitDiff), and the binary's Zod schema declares `type` as ee([\"create\",\"update\"]) with the description \"Whether a new file was created or an existing file was updated\". `type` is present only on the Write shape AMONG FILE-MUTATION CARRIERS - i.e. among toolUseResult objects that carry a top-level `filePath` (16274 corpus-wide: 2889 Write with type, 13372 Edit without, 13 ExitPlanMode without). It is NOT unique to Write across all tool results: the Read family also sets a `type` (text/image/file_unchanged/parts/pdf, 9203 records), but those nest their payload under a `file` key and carry neither a top-level `filePath` nor a top-level `content`, so they never reach csift's carrier gate.",
"depends": "csift `files` reads that `type` to decide create-vs-edit and csift `recover` uses the presence-of-`type`/absence-of-`oldString` to treat the echo as a full-content anchor; a `type` added to Edit results would turn every edit into a snapshot.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "149-156",
"snippet": " // A Write result: full-content anchor.\n if let Some(content) = tur.get(\"content\").and_then(serde_json::Value::as_str) {\n let total = line_count(content);\n events.push(FileEvent {\n line_no,\n turn_index,\n timestamp_utc: ts.clone(),\n kind: EventKind::FullSnapshot {"
},
{
"path": "src/model/mutation.rs",
"lines": "196",
"snippet": " let is_create = probe.r#type.as_ref().and_then(serde_json::Value::as_str) == Some(\"create\");"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -F 'ee([\"create\",\"update\"])' | head -1 | cut -c1-1400\nExpected: the Write-result schema literal `c({type:ee([\"create\",\"update\"]).describe(\"Whether a new file was created or an existing file was updated\"),filePath:i()...,content:i()...,structuredPatch:R(...),originalFile:i().nullable()...,gitDiff:...optional(),userModified:M().optional()...})`. Counting rule: one grep hit is the single minified schema constructor; the Edit-result constructor in the same binary (`grep -F 'oldString:i().describe'`) has no `type` member.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.6 (the `files` mutation-source table) and section 6.7"
},
"first_seen_claude_code": "2.1.156",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -cF 'ee([\"create\",\"update\"])' and the same grep -F printing the hit; (2) strings -a <the same binary> | grep -cF 'oldString:i().describe' plus the printed hit; (3) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter tallying every toolUseResult dict that carries a 'type' key, by value; (4) the same sweep restricted to toolUseResult dicts carrying a top-level 'filePath', bucketed by (has 'type', has 'content', has 'oldString')",
"observed": "grep count 1 for ee([\"create\",\"update\"]). Write output schema, verbatim: c({type:ee([\"create\",\"update\"]).describe(\"Whether a new file was created or an existing file was updated\"),filePath:i().describe(\"The path to the file that was written\"),content:i().describe(\"The content that was written to the file\"),structuredPatch:R(SQe()).describe(\"Diff patch showing the changes (empty when nothing changed, the diff timed out, or — with originalFile null on an update — the previous content was too large to diff)\"),originalFile:i().nullable().describe(\"The original file content before the write (null for new files, or when the previous content was too large to include)\"),gitDiff:kQe().optional(),userModified:M().optional().describe(\"True when the user edited the proposed content in the permission dialog before accepting\")}) . Edit output schema (grep count 1), verbatim, and it has no `type` member: c({filePath:i().describe(\"The file path that was edited\"),oldString:i().describe(\"The original string that was replaced\"),newString:i().describe(\"The new string that replaced it\"),originalFile:i().nullable().describe(\"The original file contents before editing\"),structuredPatch:R(SQe()).describe(\"Diff patch showing the changes\"),userModified:M().describe(\"Whether the user modified the proposed changes\"),replaceAll:M().describe(\"Whether all occurrences were replaced\"),gitDiff:kQe().optional()}) . BUT corpus-wide, toolUseResult.type is NOT unique to Write: 8609 objects carry type=='text', 444 'image', 120 'file_unchanged', 26 'parts', 4 'pdf' - versus 2598 'create' and 291 'update'. Those 9203 non-Write objects are the Read family, keyed {file,type} (8609/8609 text, 444/444 image, 116/120 file_unchanged) with NO top-level 'filePath' and NO top-level 'content' on any of them. Restricting to the 16274 toolUseResult objects that DO carry a top-level 'filePath': 13372 have oldString and no type and no content (Edit), 2889 have type in {create,update} plus content and no oldString (Write), and 13 have none of the three (ExitPlanMode results, keyed filePath/plan/hasTaskTool/isAgent[/planWasEdited]).",
"rule": "One grep hit = one minified schema constructor. Corpus: one tally per toolUseResult dict; the filePath partition counts each such dict once under (type present, content present, oldString present).",
"note": "The schema half of the claim is exact - both constructors quoted above are single grep hits in 2.1.258, and the Edit constructor demonstrably has no `type` member. Only the uniqueness wording overreached. The depends survives that correction intact, and for a structural reason worth recording: csift reads `type` only AFTER a filePath match (src/recover/carriers.rs:93-95 gates on `tur.get(\"filePath\")` + path_matches before anything else; src/model/mutation.rs:190-196 returns early unless probe.file_path is a non-empty string), and no Read-family result carries a top-level filePath. Both code sites verified verbatim at the claimed lines (carriers.rs:149-156, mutation.rs:196)."
}
]
},
{
"id": "REC-056",
"area": "record-model",
"behavior": "On a `Write` that CREATES a file the persisted echo is `{type:\"create\", filePath, content, structuredPatch:[], originalFile:null, userModified}` - `originalFile` is present but JSON `null` by definition on a create, and `structuredPatch` is the EMPTY array; the schema documents `originalFile` as null for new files or when the previous content was too large to include.",
"depends": "csift `recover` never consults `originalFile`/`structuredPatch` on a Write (it anchors on `content`), and `parse_structured_patch` returning `Some(vec![])` is defused by the `!patches.is_empty()` guard in `apply_edit` - without that guard an empty patch array would silently no-op an edit.",
"code": [
{
"path": "src/recover/buffer.rs",
"lines": "137-143",
"snippet": " if let Some(patches) = structured_patch {\n if !patches.is_empty() {\n return apply_structured_patch(buf, patches, line_no);\n }\n }\n // Fallback: string replacement over the dense known text.\n apply_string_edit(buf, hunks, line_no)"
},
{
"path": "src/recover/carriers.rs",
"lines": "91-93",
"snippet": " // ── (2) Write result: {type:create|update, filePath, content, …} ──\n // ── (3) Edit result: {filePath, oldString, newString, structuredPatch, …} (no type) ──\n let path = tur.get(\"filePath\").and_then(serde_json::Value::as_str);"
}
],
"instrument": "Replace the live 120-newest-file window (which reported 1801 creates / 247 updates and drifts every run) with the whole-corpus sweep, which a stranger can rerun deterministically against a fixed corpus: 2598 creates, all in the ('create', originalFile null, structuredPatch []) bucket; 291 updates, none in it.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now; SPEC.md section 6.7 extraction table; describe string measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts whose 'type' is 'create' or 'update' and which have a 'content' key, tallied by (type, originalFile is None, structuredPatch == []) and separately by ','.join(sorted(toolUseResult.keys())); plus the Write output-schema grep from REC-055",
"observed": "Corpus-wide: 2598 create echoes, ALL of them (2598/2598) with originalFile JSON null AND structuredPatch the empty array - the bucket ('create', originalFile-null=True, structuredPatch-empty=True) is 2598 and no other create bucket exists. 291 update echoes, none of which is (null, empty): 226 are (not-null, non-empty) and 65 are (null, non-empty). The `originalFile` KEY is present on 2598/2598 creates (key sets 'content,filePath,originalFile,structuredPatch,type,userModified' 2540 and the same plus 'memdirStamped' 58) - it is present-and-null, never absent. Schema text, verbatim: originalFile:i().nullable().describe(\"The original file content before the write (null for new files, or when the previous content was too large to include)\") and structuredPatch:R(SQe()).describe(\"Diff patch showing the changes (empty when nothing changed, the diff timed out, or — with originalFile null on an update — the previous content was too large to diff)\").",
"rule": "One tally per Write echo, defined as a toolUseResult dict with type in {create,update} AND a 'content' key, keyed by (type, originalFile is null, structuredPatch == []).",
"note": "The invariant is stronger than the claim's window suggested: it is 2598/2598 creates corpus-wide, with no exception at any Claude Code version present (2.1.156 through 2.1.258). One extra key, `memdirStamped`, rides 58 of the creates - worth knowing before anyone pins the create key set. Both csift code sites verified verbatim at the claimed lines: src/recover/buffer.rs:137-143 (the `if !patches.is_empty()` guard sits at line 138) and src/recover/carriers.rs:91-93. The guard's necessity is real on this data - every create ships structuredPatch [], so csift's parse_structured_patch yields Some(empty) on 2598 records and only the guard keeps them from short-circuiting the string-replacement fallback."
}
]
},
{
"id": "REC-057",
"area": "record-model",
"behavior": "On a `Write` that UPDATES an existing file, `originalFile` is a string only sometimes - corpus-wide 226 of 291 observed updates carry the pre-write text and 65 carry null. The null is not merely the schema's discretionary \"previous content was too large to include\" branch: the transcript writer applies a hard cap on the append path, rewriting originalFile to null whenever it is a string longer than 10000 characters, and 19 of the 65 nulls are instead the stripped form (content == '' as well, see REC-058). Excluding those, 46 of 272 content-bearing updates (16.9%) lack the pre-image.",
"depends": "csift `recover` builds its Write anchor from `content` alone, so an absent `originalFile` costs nothing there; but any future consumer that assumed an update always carries its pre-image would fabricate a wrong pre-state on 23% of updates.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "124-127",
"snippet": " original_file: tur\n .get(\"originalFile\")\n .and_then(serde_json::Value::as_str)\n .map(str::to_string),"
}
],
"instrument": "Use the whole-corpus cross-tab (226 / 46 / 19 of 291) rather than the live 120-newest-file window that reported 190 str vs 57 null of 247 and drifts every run.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts with type=='update' and a 'content' key, cross-tabulated by (content == '', originalFile is None); plus, over every toolUseResult dict corpus-wide, the length distribution of originalFile when it is a string; plus strings -a <the Claude Code 2.1.258 binary> searched for the transcript-append path",
"observed": "291 update echoes corpus-wide: 226 carry the pre-write text (originalFile a non-empty string), 65 carry JSON null. Cross-tab: (content non-empty, originalFile not null) 226, (content non-empty, originalFile null) 46, (content == '', originalFile null) 19. So 65/291 = 22.3% of updates lack the pre-image, and among the 272 updates that still carry their content, 46 (16.9%) lack it. Mechanism, verbatim from the binary: var rts=1e4;function Tor(e){if(typeof e!==\"object\"||e===null)return e;let n=e;if(typeof n.originalFile===\"string\"&&n.originalFile.length>rts)return{...n,originalFile:null};return e} - and the transcript writer applies it on the append path: if(ke.type===\"user\"&&ke.toolUseResult!=null)ke.toolUseResult=Tor(ke.toolUseResult);if(await this.appendEntry(ke,C,I,v),...) . Corpus check of that cap: across 3908 persisted toolUseResult.originalFile strings the longest is 9997 characters and 0 exceed 10000 (Write echoes alone: longest 9693, 0 over 10000).",
"rule": "One tally per type=='update' Write echo, keyed by (content == '', originalFile is null). Cap check: one measurement per toolUseResult dict whose originalFile is a string, bucketed by len(); the counting rule for the cap is max(len(originalFile)) and count(len > 10000).",
"note": "The claim's shape is right - a Write update's pre-image is present most of the time and missing often enough to matter - and its ~23% null rate reproduces (22.3% corpus-wide). What it missed is the mechanism and a confound. The mechanism is a measurable constant: a 10000-character cap applied at append time to EVERY user record's toolUseResult, which the corpus confirms cleanly (max persisted originalFile 9997 chars, zero above 10000, over 3908 strings). The confound is that 19 of the 65 nulls are the stripped update form, so a consumer that treats null as 'file was too big' is wrong on 29% of them. csift code site verified verbatim at the claimed lines: src/recover/carriers.rs:124-127."
}
]
},
{
"id": "REC-058",
"area": "record-model",
"behavior": "Claude Code's Write tool defines a stripForStorage rewrite - stripForStorage(e){... if(e.type!==\"update\")return e; if(e.content===\"\"&&(e.originalFile??\"\")===\"\")return e; if(Array.isArray(e.structuredPatch)&&e.structuredPatch.length===0&&e.originalFile===null)return e; return{...e,content:\"\",originalFile:null}} - so an update-type Write carrier CAN carry an empty `content` string and no original content. Two guards exempt it, not one: an already-empty pair, and an empty structuredPatch with a null originalFile. The rewrite is driven over a message LIST by a function that only touches entries older than the newest 200 (a headless drain passes 0, stripping all), and in 2.1.258 the transcript append path itself applies only the 10000-character originalFile cap, not this strip. On disk the empty-content update carrier is confined to two versions: 19 records, 16 at 2.1.208 and 3 at 2.1.211, and none among the 102 update carriers recorded at 2.1.217 through 2.1.258.",
"depends": "csift's carrier extractor treats any `toolUseResult` with a `filePath` and a `content` string but no `oldString`/`newString` as a full-snapshot anchor and calls `buf.reset_to_full(content, ...)`, so a stripped update carrier resets the sparse reconstruction buffer to a zero-line file and every line known before it becomes an explicit gap.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "149-151",
"snippet": " // A Write result: full-content anchor.\n if let Some(content) = tur.get(\"content\").and_then(serde_json::Value::as_str) {\n let total = line_count(content);"
},
{
"path": "src/recover/replay.rs",
"lines": "89",
"snippet": " buf.reset_to_full(content, *total_lines, e.line_no);"
}
],
"instrument": "Add the version key to the tally - the phenomenon is version-confined and a version-blind count hides that. Corpus-wide (not the 120-newest window): 291 update carriers, 19 empty; 2598 create carriers, 1 empty.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": "2.1.208",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -o '.\\{260\\}stripForStorage(e){[^}]\\{0,420\\}' ; (2) a python find over the same strings dump for the call sites of the driver function and for the transcript-append path; (3) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter tallying Write echoes (toolUseResult.type in {create,update} with a 'content' key) bucketed by whether content == '' and by the record's `version` field",
"observed": "Write tool, verbatim and complete: stripForStorage(e){if(typeof e!==\"object\"||e===null)return e;if(e.type!==\"update\")return e;if(e.content===\"\"&&(e.originalFile??\"\")===\"\")return e;if(Array.isArray(e.structuredPatch)&&e.structuredPatch.length===0&&e.originalFile===null)return e;return{...e,content:\"\",originalFile:null}} - two early-return guards, not one. Driver, verbatim: function $_t(e,n,r=200,o=!1){let d=e.length-r;if(d<=0)return e; ... if(v>=d||C.type!==\"user\"||C.isVirtual||C.toolUseResult==null||!Array.isArray(C.message.content))continue; ... if(!F?.stripForStorage)continue; ... _[v]={...C,toolUseResult:B}} - it rewrites a message LIST and only touches entries older than the newest `r` (default 200). Its two call sites are a headless drain (called with r=0, o=true, which strips every entry) and an interactive query_end updater; neither is the file-append path. The append path applies a different transform: if(ke.type===\"user\"&&ke.toolUseResult!=null)ke.toolUseResult=Tor(ke.toolUseResult) before this.appendEntry(ke,...), i.e. only the 10000-character originalFile cap. Corpus: 291 update carriers, 19 with content == '' - 16 at version 2.1.208 and 3 at 2.1.211, and ZERO at any later version, against 102 update carriers recorded at versions 2.1.217 through 2.1.258. 2598 create carriers with exactly 1 empty content (at 2.1.217; a genuinely empty file). All 19 stripped updates also carry originalFile null.",
"rule": "One count per carrier RECORD (a toolUseResult dict with type in {create,update} and a 'content' key), bucketed by content == '' and keyed by the record's top-level `version`.",
"note": "Not marked drifted, because the rule is still verbatim in the 2.1.258 binary and the on-disk artifact is real; but the claim's 'STRIPS ... before persisting it' is the part the instruments do not support for the current version. What would decide it: a session run on 2.1.258 that performs a Write of type update more than 200 messages before the session ends, then re-reading that record from the transcript to see whether content survived. A plausible path by which the stripped form reached disk in the 2.1.208 era is the writer's own session-file compaction (requestCompact(this.sessionFile,...)), which rewrites the file from the in-memory list. The depends is confirmed in code and is exact: src/recover/carriers.rs:149-151 accepts any content string including \"\", src/recover/diff.rs:167-178 makes line_count(\"\") == 0 (split_lines returns an empty Vec on empty input), and src/recover/replay.rs:89 then calls buf.reset_to_full(content, 0, line_no) - a zero-line reset. Both claimed code sites verified verbatim."
}
]
},
{
"id": "REC-059",
"area": "record-model",
"behavior": "An `Edit` tool result echoes toolUseResult as {filePath, oldString, newString, originalFile (nullable), structuredPatch, userModified, replaceAll} and carries NO `type` key at all - the absence of `type` is what distinguishes it from a Write carrier. Two optional extras are actually emitted on top of that seven-key core: `staleRecovered` (92 of 13372) and `memdirStamped` (43 of 13372). The `gitDiff` the schema declares optional was observed on 0 of 13372 echoes, so it is a declared-but-unseen field on this corpus, not an expected one.",
"depends": "csift `recover` discriminates an Edit from a Write purely by `tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some()` after the `filePath` match; a `type` appearing on Edit results, or a rename of the two string keys, silently reclassifies every edit as a full-content snapshot and produces a plausible WRONG reconstruction rather than an error.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "91-98",
"snippet": " // ── (2) Write result: {type:create|update, filePath, content, …} ──\n // ── (3) Edit result: {filePath, oldString, newString, structuredPatch, …} (no type) ──\n let path = tur.get(\"filePath\").and_then(serde_json::Value::as_str);\n if !path_matches(target_file, path.unwrap_or_default()) {\n return;\n }\n let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));"
},
{
"path": "src/recover/carriers.rs",
"lines": "97-98",
"snippet": " let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));"
}
],
"instrument": "Prefer the whole-corpus sweep (13372 Edit echoes, `type` on 0) over the 120-newest-file window (11299 modal-tuple hits) - the window is live and drifts; the corpus-wide top-level-only count is 11421 and subagent transcripts add 1951 more.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.7 (`Edit / MultiEdit | toolUseResult.{oldString,newString,structuredPatch,originalFile} (no type)`); measured now"
},
"first_seen_claude_code": "2.1.150",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -cF 'oldString:i().describe' and the printed hit; (2) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts that contain an 'oldString' key and tallying ','.join(sorted(keys)), plus a separate count of how many contain a 'type' key",
"observed": "Edit output schema (grep count 1), verbatim: c({filePath:i().describe(\"The file path that was edited\"),oldString:i().describe(\"The original string that was replaced\"),newString:i().describe(\"The new string that replaced it\"),originalFile:i().nullable().describe(\"The original file contents before editing\"),structuredPatch:R(SQe()).describe(\"Diff patch showing the changes\"),userModified:M().describe(\"Whether the user modified the proposed changes\"),replaceAll:M().describe(\"Whether all occurrences were replaced\"),gitDiff:kQe().optional()}) - no `type` member. Corpus: 13372 Edit echoes; `type` present on 0 of 13372. Key sets: 'filePath,newString,oldString,originalFile,replaceAll,structuredPatch,userModified' 13237 (99.0%), the same plus 'staleRecovered' 92, the same plus 'memdirStamped' 43. `gitDiff` observed on 0 of 13372. structuredPatch present on 13372/13372; replaceAll present on 13372/13372.",
"rule": "One tally per Edit echo, defined as a toolUseResult dict containing an 'oldString' key; buckets are the sorted key tuple, and 'type present' is a separate count over the same population.",
"note": "The discriminator holds absolutely on this corpus: 0 of 13372 Edit echoes carry a `type`, and the 2.1.258 Edit schema constructor has no `type` member, so csift's oldString/newString test cannot be fooled by present data. The claim's corpus-wide figures reproduce exactly where it gave them (structuredPatch on 13372; replaceAll:true on 70). The only real correction is the key list: `memdirStamped` is an emitted key the claim does not name, and `gitDiff` - which it does name - never appears. Both code sites verified verbatim at the claimed lines (src/recover/carriers.rs:91-98 and 97-98)."
}
]
},
{
"id": "REC-060",
"area": "record-model",
"behavior": "An `Edit` result's `originalFile` is schema-declared .nullable() (\"The original file contents before editing\") and is null in the MAJORITY of real echoes: 9690 null against 3682 non-null across 13372 Edit echoes corpus-wide. The dominant cause is not editorial - the transcript writer applies a hard 10000-character cap on the append path, rewriting any string originalFile longer than that to null before the record is written. The corpus bears this out: across 3908 persisted originalFile strings the longest is 9997 characters and none exceeds 10000. A third state also exists that a null-vs-string test misses: 209 of the 3682 non-null values are the EMPTY string (the Edit tool's own stripForStorage rewrites originalFile to \"\"), so only 3473 echoes - 26.0% - carry usable pre-image text.",
"depends": "csift `recover`'s AUTHORITATIVE `original_file_disagreement` boundary can only fire on the ~26% of edits that carry the string, so a clean `recover --coverage` is evidence over that fraction, not over every edit; the check is silently skipped, never reported as skipped.",
"code": [
{
"path": "src/recover/replay.rs",
"lines": "131-136",
"snippet": " // Boundary cross-check: originalFile vs replayed buffer.\n if let Some(orig) = original_file {\n if had_full_anchor && buffer_disagrees_with_original(&buf, orig) {\n out.boundaries.push(Boundary {\n line_no: e.line_no,\n turn_index: e.turn_index,"
},
{
"path": "src/recover/replay.rs",
"lines": "531-535",
"snippet": "pub(crate) fn buffer_disagrees_with_original(buf: &SparseBuffer, original_file: &str) -> bool {\n let orig_lines = split_lines(original_file);\n if orig_lines.is_empty() {\n return false;\n }"
}
],
"instrument": "Tally three states, not two (null / empty string / non-empty string), and add the length-bucket sweep that exposes the 10000-character cap. Prefer corpus-wide counts over the live 120-newest window (which reported 8443/2975 of 11418).",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -cF 'originalFile:i().nullable().describe(\"The original file contents before editing\")' ; (2) a python find over the same strings dump for the transcript-append transform; (3) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts with an 'oldString' key and tallying originalFile as one of three states - JSON null, the empty string, a non-empty string; (4) the same sweep bucketing len(originalFile) for every toolUseResult dict whose originalFile is a string",
"observed": "Schema grep returns 1. Corpus: 13372 Edit echoes - 9690 originalFile null, 209 originalFile the EMPTY string, 3473 a non-empty string. (9690 null vs 3682 non-null reproduces the claim's corpus-wide pair exactly; the claim's '2975 string' window figure lumps the empty ones in.) So only 3473/13372 = 26.0% carry usable pre-image text. Cap mechanism, verbatim from the binary: var rts=1e4;function Tor(e){...if(typeof n.originalFile===\"string\"&&n.originalFile.length>rts)return{...n,originalFile:null};return e} applied as if(ke.type===\"user\"&&ke.toolUseResult!=null)ke.toolUseResult=Tor(ke.toolUseResult) immediately before this.appendEntry(ke,...). Corpus confirmation of the cap: over all 3908 persisted toolUseResult.originalFile strings the maximum length is 9997 characters and the count above 10000 is 0; the length buckets stop hard (<=9500: 146, <=9900: 144, <=10000: 39, >10000: 0). Empty-string originalFile appears at versions 2.1.159 through 2.1.221 (84 at 2.1.208, 42 at 2.1.210, 26 at 2.1.220, ...).",
"rule": "One tally per Edit echo (toolUseResult dict with an 'oldString' key), keyed by which of three states originalFile is in. Cap rule: over every toolUseResult dict whose originalFile is a string, report max(len) and count(len > 10000).",
"note": "The claim's headline - originalFile is null in the majority, so csift's authoritative original_file_disagreement boundary can only fire on about a quarter of edits - stands, and its corpus-wide numbers reproduce exactly. Two refinements matter for anyone reasoning about WHY. First, the nullness is a mechanical size cap with a measurable constant, not a discretionary schema branch: an edit to any file whose pre-image exceeds 10000 characters is silently unverifiable, which makes the un-checked fraction correlate with file size rather than being random. Second, the 209 empty-string values would pass a naive isinstance(str) test; csift is defused here, not by luck but by an explicit guard - src/recover/replay.rs:529-533 returns false when split_lines(original_file) is empty, so an empty pre-image can never raise a false boundary. Both claimed code sites verified verbatim at the claimed lines (replay.rs:131-136 and 529-533)."
}
]
},
{
"id": "REC-061",
"area": "record-model",
"behavior": "`replaceAll` on the Edit echo is the boolean `Whether all occurrences were replaced` and is the ONLY signal distinguishing a one-shot replacement from a global one.",
"depends": "csift `recover`'s string-replacement fallback branches on it (`text.replace` versus `text.replacen(.., 1)`); if the key were dropped the `unwrap_or(false)` default would silently turn every global rename into a single replacement and produce a plausible wrong file.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "113-117",
"snippet": " replace_all: tur\n .get(\"replaceAll\")\n .and_then(serde_json::Value::as_bool)\n .unwrap_or(false),\n }];"
},
{
"path": "src/recover/types.rs",
"lines": "149-155",
"snippet": "/// One hunk of an Edit (old→new strings), from the tool_use `input`.\n#[derive(Debug, Clone)]\npub(crate) struct EditHunk {\n pub(crate) old_string: String,\n pub(crate) new_string: String,\n pub(crate) replace_all: bool,\n}"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -F 'replaceAll:M().describe(\"Whether all occurrences were replaced\")' | wc -l # expect 1\nCounting rule: one matching `strings` line = the Edit-result schema constructor. Corpus check: one tally per Edit echo keyed by `replaceAll`; the key was present on all 11421 measured Edit echoes.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -cF 'replaceAll:M().describe(\"Whether all occurrences were replaced\")' ; (2) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts with an 'oldString' key and tallying presence and value of 'replaceAll'; (3) the same sweep, for the replaceAll==True subset only, tallying len(structuredPatch)",
"observed": "Schema grep returns 1, and the printed literal is exactly replaceAll:M().describe(\"Whether all occurrences were replaced\"). Corpus: 13372 Edit echoes, `replaceAll` present on 13372/13372 (0 missing at any version), value false on 13302 and true on 70. Of those 70 global replacements, the structuredPatch hunk count is 1 on 16 of them (and 2 on 25, 4 on 13, 9-13 on 7, other counts on the rest).",
"rule": "One tally per Edit echo (toolUseResult dict with an 'oldString' key), keyed by whether 'replaceAll' is present and by its value; the hunk tally is len(toolUseResult['structuredPatch']) over the replaceAll==True subset only.",
"note": "Holds, and the claim's own corpus figure reproduces exactly: 70 echoes with replaceAll true. The key is universal on this corpus - present on all 13372 Edit echoes, so csift's unwrap_or(false) default has never had to fire. The 'ONLY signal' wording survives an adversarial test I ran against it: the obvious alternative signal, a structuredPatch with more than one hunk, would misclassify 16 of the 70 global replacements as one-shot, because a replace_all that happens to touch one contiguous region produces exactly one hunk. So there really is no substitute for the flag. Both code sites verified verbatim at the claimed lines (src/recover/carriers.rs:113-117 and src/recover/types.rs:149-155)."
}
]
},
{
"id": "REC-062",
"area": "record-model",
"behavior": "Every `structuredPatch` hunk is exactly `{oldStart, oldLines, newStart, newLines, lines}` - five keys, no more, no fewer, all schema-required and shared verbatim between the Write and Edit result shapes - where `lines` is an array of gutter-prefixed diff strings.",
"depends": "csift's `PatchHunk` mirrors those names verbatim and `parse_structured_patch` hard-requires `oldStart`/`oldLines`/`newLines` (a missing one aborts the whole patch via `?`), so a hunk-key rename drops `recover` to the string-replacement fallback with no diagnostic.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "333-339",
"snippet": "pub(crate) fn parse_structured_patch(v: Option<&serde_json::Value>) -> Option<Vec<PatchHunk>> {\n let arr = v?.as_array()?;\n let mut out = Vec::with_capacity(arr.len());\n for h in arr {\n let old_start = h.get(\"oldStart\").and_then(serde_json::Value::as_u64)? as usize;\n let old_lines = h.get(\"oldLines\").and_then(serde_json::Value::as_u64)? as usize;\n let new_lines = h.get(\"newLines\").and_then(serde_json::Value::as_u64)? as usize;"
},
{
"path": "src/recover/types.rs",
"lines": "157-164",
"snippet": "/// One structured-patch hunk (`toolUseResult.structuredPatch[]`): a mirror of CC's\n/// `{oldStart, oldLines, newStart, newLines, lines:[\" \",\"-\",\"+\", …]}`. `newStart` is not\n/// retained - replay derives the new position from `oldStart` + the running line offset.\n#[derive(Debug, Clone)]\npub(crate) struct PatchHunk {\n pub(crate) old_start: usize,\n pub(crate) old_lines: usize,\n pub(crate) new_lines: usize,"
},
{
"path": "src/recover/carriers.rs",
"lines": "337-339",
"snippet": " let old_start = h.get(\"oldStart\").and_then(serde_json::Value::as_u64)? as usize;\n let old_lines = h.get(\"oldLines\").and_then(serde_json::Value::as_u64)? as usize;\n let new_lines = h.get(\"newLines\").and_then(serde_json::Value::as_u64)? as usize;"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -cF 'c({oldStart:A(),oldLines:A(),newStart:A(),newLines:A(),lines:R(i())})' # expect 1\npython3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if 'structuredPatch' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if isinstance(t,dict) and isinstance(t.get('structuredPatch'),list):\n for h in t['structuredPatch']:\n if isinstance(h,dict): k[tuple(sorted(h))]+=1\nprint(k.most_common(4))\nEOF\nCounting rule: one tally per hunk object across every `structuredPatch` array, keyed by its sorted key tuple. Measured 2026-09-02: `('lines','newLines','newStart','oldLines','oldStart')` 12638 hunks and NO other tuple.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "src/recover/types.rs:157-167 comment; measured now"
},
"first_seen_claude_code": "2.1.150",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'c({oldStart:A(),oldLines:A(),newStart:A(),newLines:A(),lines:R(i())})' AND strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -oF 'structuredPatch:R(SQe()).describe(\"Diff patch showing the changes' | wc -l AND python3: for each of ~/.claude/projects/*/*.jsonl sorted by mtime desc, json.loads every line containing 'structuredPatch', tally each element of toolUseResult.structuredPatch by tuple(sorted(hunk_keys))",
"observed": "binary: 1 hit, the schema constructor is `SQe=m(()=>c({oldStart:A(),oldLines:A(),newStart:A(),newLines:A(),lines:R(i())}))` - five members, none carrying `.optional()`; both file-tool schemas reference it as `structuredPatch:R(SQe())` (2 hits). corpus: 12642 hunks, ALL under the single tuple ('lines','newLines','newStart','oldLines','oldStart'); no second tuple. csift code: src/recover/carriers.rs:333 `pub(crate) fn parse_structured_patch`, :337-339 the three `?`-propagating `h.get(\"oldStart\"|\"oldLines\"|\"newLines\")` reads, and src/recover/types.rs:157-164 `PatchHunk` - all verbatim at the claimed lines.",
"rule": "one tally per hunk object across every toolUseResult.structuredPatch array in the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by its sorted key tuple; binary counts are exact-literal grep -cF hits.",
"note": "Schema-required is now positively shown rather than assumed: none of the five members carries `.optional()` in the constructor, and both the Write schema (`LRo`) and the Edit schema (`gJt`) spell the member as `structuredPatch:R(SQe())`, i.e. the same constructor, so the shape is shared verbatim rather than duplicated."
}
]
},
{
"id": "REC-063",
"area": "record-model",
"behavior": "Add: the sentinel is not emitted by every patch producer. One producer in the binary strips it and then drops any hunk it emptied - `return r.hunks.map((e)=>({oldStart:e.oldStart,oldLines:e.oldLines,newStart:e.newStart,newLines:e.newLines,lines:e.lines.filter((s)=>!s.startsWith(\"\\\\\\\\\"))})).filter((e)=>e.lines.length>0)` - so a sentinel-free patch is not evidence the file ended in a newline.",
"depends": "csift's `apply_structured_patch` selects the old region with `starts_with('-') || starts_with(' ')` and the new region with `starts_with('+') || starts_with(' ')`, so the backslash sentinel falls out of both and never becomes a fabricated buffer line; a new prefix character would be dropped just as silently.",
"code": [
{
"path": "src/recover/buffer.rs",
"lines": "184-193",
"snippet": " let old_region: Vec<String> = h\n .lines\n .iter()\n .filter(|l| l.starts_with('-') || l.starts_with(' '))\n .map(|l| l[1.min(l.len())..].to_string())\n .collect();\n let added: Vec<String> = h\n .lines\n .iter()\n .filter(|l| l.starts_with('+') || l.starts_with(' '))"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -cF 'No newline at end of file' # expect 3\npython3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if 'structuredPatch' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if isinstance(t,dict) and isinstance(t.get('structuredPatch'),list):\n for h in t['structuredPatch']:\n for l in (h.get('lines') or []): k[l[:1] if l else '<empty>']+=1\nprint(k.most_common(6))\nEOF\nCounting rule: one tally per element of every hunk's `lines` array, keyed by its first character. Measured 2026-09-02: `+` 139469, ` ` 83202, `-` 30191, `\\` 21 - and no other prefix.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'No newline at end of file' AND python3: tally the first character of every element of every toolUseResult.structuredPatch hunk's `lines` array across ~/.claude/projects/*/*.jsonl, then re-tally the distinct full strings among those starting with a backslash",
"observed": "binary: 3 hits, each the literal `\\ No newline at end of file`. corpus prefixes: '+' 144642, ' ' 83814, '-' 33611, '\\\\' 23, and no other first character (no empty-string element either). All 23 backslash entries are the single distinct string `\\ No newline at end of file`. csift code: src/recover/buffer.rs:184-193 verbatim - the old region filters `starts_with('-') || starts_with(' ')`, the new region `starts_with('+') || starts_with(' ')`, so the sentinel falls out of both.",
"rule": "one tally per element of every hunk's `lines` array over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by its first character; then a second pass keyed by the full string for the backslash bucket.",
"note": "The 23 sentinel lines split 20 on Edit echoes and 3 on Write `type:\"update\"` echoes (rule: one tally per backslash-prefixed `lines` element, keyed by whether the carrying toolUseResult has `oldString`), which is consistent with those two shapes being built by producers other than the stripping one. csift's filter is unaffected either way: the sentinel is excluded from both regions, so it never becomes a fabricated buffer line."
}
]
},
{
"id": "REC-064",
"area": "record-model",
"behavior": "Split the two halves by strength. The Write-create half is a CODE guarantee: the create branch writes the literal `structuredPatch:[]`. The Edit half is EMPIRICAL only - the Edit schema requires the key but not a non-empty array, and the Edit's patch comes from a computed diff, so 11421/11421 non-empty is a measurement, not a contract.",
"depends": "Also correct the converse: an empty Write patch is NOT exclusively a create. A Write `type:\"update\"` emits `[]` on the too-large-to-diff branch (`let we=fe?[]:CTe(...)`, with `originalFile` set to null on the same branch), so `structuredPatch:[]` alone does not mean a new file.",
"code": [
{
"path": "src/recover/buffer.rs",
"lines": "292-297",
"snippet": " // Only safe when the known lines are one contiguous run starting at line 1.\n let ranges = buf.covered_ranges();\n let contiguous_from_one = matches!(ranges.first(), Some(&(1, _))) && ranges.len() == 1;\n if !contiguous_from_one {\n return EditOutcome::UnAnchorable;\n }"
}
],
"instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nmissing=empty=total=0\nfor f in files:\n for line in open(f,errors='replace'):\n if 'oldString' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if isinstance(t,dict) and 'oldString' in t:\n total+=1; sp=t.get('structuredPatch')\n if sp is None: missing+=1\n elif sp==[]: empty+=1\nprint('edits',total,'missing patch',missing,'empty patch',empty)\nEOF\nCounting rule: one tally per Edit echo. Measured 2026-09-02: 11421 edits, 0 with the key missing, 0 with an empty array.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3: over ~/.claude/projects/*/*.jsonl, for every parsed toolUseResult object, bucket `'oldString' in t` as an Edit and `t.get('type') in ('create','update') and 'content' in t` as a Write, then tally whether structuredPatch is missing / [] / non-empty AND strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'type:\"create\",filePath:e,content:n,structuredPatch:[],originalFile:null,userModified:' AND ... | grep -cF 'let we=fe?[]:CTe({filePath:e,oldContent:W,newContent:n,convertTabs:!0})'",
"observed": "corpus: 11421 Edit echoes - 0 with the key missing, 0 with an empty array. 2048 Write echoes split ('create','EMPTY') 1801 and ('update','NONEMPTY') 247; no create carried a non-empty patch and no update carried an empty one. binary: 1 hit for the hardcoded create branch `{type:\"create\",filePath:e,content:n,structuredPatch:[],originalFile:null,userModified:v??!1,...}`; 1 hit for the update branch `let we=fe?[]:CTe({filePath:e,oldContent:W,newContent:n,convertTabs:!0})`. csift code: src/recover/buffer.rs:292-297 verbatim (`contiguous_from_one` guard returning `EditOutcome::UnAnchorable`).",
"rule": "one tally per file-tool echo over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by shape and by the structuredPatch state (absent / [] / non-empty).",
"note": "Corroborating binary detail: a renderer detects that degenerate update explicitly - `i===\"update\"&&Array.isArray(s)&&s.length===0&&l===null&&typeof a===\"string\"&&a!==\"\"` (1 hit) - and re-renders it as a create, which is exactly the ambiguity REC-065 documents."
}
]
},
{
"id": "REC-065",
"area": "record-model",
"behavior": "A `Write` result's `structuredPatch` is documented by its own schema as \"empty when nothing changed, the diff timed out, or - with originalFile null on an update - the previous content was too large to diff\", so an empty patch array is ambiguous between three unrelated causes.",
"depends": "csift parses `structuredPatch` into positional hunks and falls back to a string replacement when it is absent or unusable, and an edit whose anchor neighbourhood is unknown becomes a counted un-anchorable coverage hole rather than a fabricated line - which is why an empty patch degrades coverage rather than corrupting content.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "333-336",
"snippet": "pub(crate) fn parse_structured_patch(v: Option<&serde_json::Value>) -> Option<Vec<PatchHunk>> {\n let arr = v?.as_array()?;\n let mut out = Vec::with_capacity(arr.len());\n for h in arr {"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o 'structuredPatch:R(SQe()).describe(\"Diff patch showing the changes[^\"]*\"' - expect two describe variants, the Write one carrying the three-cause caveat and the Edit one the bare form. Counting rule: distinct describe literals, deduplicated.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -o 'Diff patch showing[^\"]*' | sort -u AND strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -oF 'structuredPatch:R(SQe()).describe(\"Diff patch showing the changes' | wc -l",
"observed": "exactly two distinct describe literals, deduplicated: `Diff patch showing the changes` and `Diff patch showing the changes (empty when nothing changed, the diff timed out, or \\u2014 with originalFile null on an update \\u2014 the previous content was too large to diff)`; the `structuredPatch:R(SQe()).describe(` prefix occurs 2 times, once per schema. Reading the surrounding schema bodies puts the bare form on the Edit schema (`gJt=m(()=>c({filePath:i().describe(\"The file path that was edited\"),...`) and the three-cause form on the Write schema (`LRo=m(()=>c({type:ee([\"create\",\"update\"])...`). csift code: src/recover/carriers.rs:333-336 verbatim.",
"rule": "distinct describe string literals in the binary, deduplicated with sort -u; then one attribution per literal by which schema constructor body encloses it.",
"note": "The three causes are separately corroborated in code: the too-large cause is the ternary `let we=fe?[]:CTe(...)` on the update branch (same `fe` flag also nulls `originalFile`, which is why the caveat pairs them), and a downstream renderer detects that exact degenerate shape. Nothing in the echo distinguishes the three, so csift is right to treat an empty patch as a coverage hole rather than a signal."
}
]
},
{
"id": "REC-066",
"area": "record-model",
"behavior": "Drop the word 'only'. Two further keys discriminate just as cleanly in the measured corpus and in both schemas: `replaceAll` is on 11421/11421 Edit echoes and 0/2048 Write echoes, and `content` is on 2048/2048 Write echoes and 0/11421 Edit echoes. The accurate statement is that `structuredPatch`, `originalFile` and `userModified` are the three shared keys, while `type`+`content` (Write) and `oldString`/`newString`+`replaceAll` (Edit) are each sufficient discriminators.",
"depends": "csift `recover` checks `oldString`/`newString` FIRST and only falls through to the `content` full-snapshot arm, so the shared keys are harmless today; a discriminator rewritten to key on `structuredPatch` presence would classify every Write as an edit.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "97-101",
"snippet": " let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));\n\n if has_edit_strings {\n // An Edit (carrier side): keep the strings + structuredPatch + originalFile."
}
],
"instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if '\"toolUseResult\"' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if not isinstance(t,dict): continue\n if 'oldString' in t: k[('edit', 'structuredPatch' in t, 'originalFile' in t, 'userModified' in t)]+=1\n elif t.get('type') in ('create','update') and 'content' in t: k[('write', 'structuredPatch' in t, 'originalFile' in t, 'userModified' in t)]+=1\nprint(k.most_common(4))\nEOF\nCounting rule: one tally per file-tool echo, keyed by shape and by presence of the three shared keys. Measured 2026-09-02: `('edit',True,True,True)` 11421 and `('write',True,True,True)` 2048 - no other tuple.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3: over ~/.claude/projects/*/*.jsonl, key every Edit echo (`'oldString' in t`) and every Write echo (`t.get('type') in ('create','update') and 'content' in t`) by (shape, 'structuredPatch' in t, 'originalFile' in t, 'userModified' in t), and separately tally every top-level key of each echo AND strings -n 6 -a <the Claude Code 2.1.258 binary>, reading the `gJt` (Edit) and `LRo` (Write) schema bodies",
"observed": "corpus: exactly two tuples - ('edit',True,True,True) 11421 and ('write',True,True,True) 2048; no third tuple, so none of the three keys discriminates. Per-key census, Edit echoes: filePath/oldString/newString/originalFile/structuredPatch/userModified/replaceAll all 11421, staleRecovered 80, memdirStamped 41. Write echoes: type/filePath/content/structuredPatch/originalFile/userModified all 2048, memdirStamped 49. binary: Edit schema `c({filePath,oldString,newString,originalFile:i().nullable(),structuredPatch:R(SQe()),userModified:M(),replaceAll:M(),gitDiff:kQe().optional()})`; Write schema `c({type:ee([\"create\",\"update\"]),filePath,content,structuredPatch:R(SQe()),originalFile:i().nullable(),gitDiff:kQe().optional(),userModified:M().optional()})`. csift code: src/recover/carriers.rs:97-101 verbatim.",
"rule": "one tally per file-tool echo over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by shape and by presence of the three shared keys; plus one tally per (shape, top-level key) pair.",
"note": "One asymmetry the claim does not mention and which matters if anyone keys on presence: `userModified` is REQUIRED on the Edit schema (`userModified:M()`) but OPTIONAL on the Write schema (`userModified:M().optional()`). It was present on all 2048 measured Write echoes anyway, because the Write builders always spread `userModified:v??!1`, but a consumer must not rely on that."
}
]
},
{
"id": "REC-067",
"area": "record-model",
"behavior": "The absence is explained, not merely unwitnessed. The Edit result builder computes the member only inside `if(a.CLAUDE_CODE_REMOTE){...}` and then spreads it conditionally as `...De&&{gitDiff:De}`, so `gitDiff` is emitted only by remote Claude Code sessions. A local session can never produce it, which is why the local corpus reads 0 and will keep reading 0.",
"depends": "csift's carrier extractor reads only the keys it names, so an unmodelled `gitDiff` is inert today; it matters as a warning that the echo is an OPEN object and a future `type`-like discriminator could arrive inside it.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "93-98",
"snippet": " let path = tur.get(\"filePath\").and_then(serde_json::Value::as_str);\n if !path_matches(target_file, path.unwrap_or_default()) {\n return;\n }\n let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));"
}
],
"instrument": "Add the deciding condition to the instrument so a stranger knows what would flip it: `strings -n 6 -a <binary> | grep -cF 'if(a.CLAUDE_CODE_REMOTE)'` (14 hits) plus the `...De&&{gitDiff:De}` spread in the Edit result object.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'kQe=m(()=>c({filename:i(),status:ee([\"modified\",\"added\"]),additions:A(),deletions:A(),changes:A(),patch:i(),repository:i().nullable().optional()' AND ... | grep -oF 'gitDiff:kQe().optional()' | wc -l AND ... | grep -cF 'if(a.CLAUDE_CODE_REMOTE)' AND python3: count raw jsonl lines containing the substring 'gitDiff', and count parsed toolUseResult objects carrying the key",
"observed": "binary: 1 hit for the constructor `kQe=m(()=>c({filename:i(),status:ee([\"modified\",\"added\"]),additions:A(),deletions:A(),changes:A(),patch:i(),repository:i().nullable().optional()}))`; 2 hits for `gitDiff:kQe().optional()`, one in each file-tool schema. corpus: 0 raw lines containing 'gitDiff' and 0 echoes carrying the key. csift code: src/recover/carriers.rs:93-98 verbatim.",
"rule": "binary: exact-literal grep -cF hits. corpus: one tally per raw jsonl line containing the substring `gitDiff`, and one per parsed toolUseResult object with the key, over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist).",
"note": "The 'open object' warning in the depends field is sound and is independently demonstrated by REC-069: `memdirStamped` and `staleRecovered` both ride on the echo without appearing in either zod schema."
}
]
},
{
"id": "REC-068",
"area": "record-model",
"behavior": "The behavior text says 11418 measured Edit echoes while its own instrument says 11421; the measured figure now is 11421 with 0 true. Use one number.",
"depends": "Add the provenance split: in the tool path the field is computed as `userModified:_??!1` (Edit) / `userModified:v??!1` (Write), but a separate record-reconstruction path sets `userModified:r.editedByApproval`. Both feed the same key, so a consumer reading it cannot tell which path produced it.",
"code": [
{
"path": "src/model/mutation.rs",
"lines": "146-152",
"snippet": " let (op, key) = match name {\n \"Write\" => (FileOp::Write, \"file_path\"),\n \"Edit\" => (FileOp::Edit, \"file_path\"),\n \"MultiEdit\" => (FileOp::MultiEdit, \"file_path\"),\n \"NotebookEdit\" => (FileOp::NotebookEdit, \"notebook_path\"),\n _ => continue,\n };"
},
{
"path": "src/recover/events.rs",
"lines": "86-92",
"snippet": " if let Some(content) = input.get(\"content\").and_then(serde_json::Value::as_str) {\n events.push(FileEvent {\n line_no,\n turn_index,\n timestamp_utc: ts.clone(),\n kind: EventKind::FullSnapshot {\n content: content.to_string(),"
}
],
"instrument": "Tally every Edit echo by its `userModified` value. Counting rule: one tally per carrier record. Measured 2026-09-02: `False` 11421, `True` 0 over the 120 most-recently-modified top-level transcripts, and 0 true corpus-wide - the branch is schema-attested but not corpus-witnessed here. Schema authority: `strings -a <the Claude Code 2.1.258 binary> | grep -F 'userModified:M().describe(\"Whether the user modified the proposed changes\")' | wc -l` returns 1.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3: tally `t.get('userModified')` over every Edit echo (`'oldString' in t`) in ~/.claude/projects/*/*.jsonl AND strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'userModified:M().describe(\"Whether the user modified the proposed changes\")' AND ... | grep -cF 'userModified:M().optional().describe(\"True when the user edited the proposed content in the permission dialog before accepting\")' AND ... | grep -oF 'The user modified your proposed content before accepting it.' | wc -l AND ... | grep -oF 'The user modified your proposed changes before accepting them.' | wc -l",
"observed": "corpus: {False: 11421}, i.e. 11421 Edit echoes all false and 0 true. binary: the Edit describe literal 1 hit; the Write describe literal `userModified:M().optional().describe(\"True when the user edited the proposed content in the permission dialog before accepting\")` 1 hit; each prose note 2 hits. The Edit note is emitted as `let ... v=o?\". The user modified your proposed changes before accepting them. \":\"\"` inside the Edit `mapToolResultToToolResultBlockParam`, the Write note as `let f=r?\" The user modified your proposed content before accepting it.\":\"\"` inside the Write one - both with exactly the spacing the claim records. csift code: src/model/mutation.rs:146-152 and src/recover/events.rs:86-92 verbatim.",
"rule": "one tally per Edit carrier record over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by the boolean value; binary counts are exact-literal grep hits.",
"note": "The true branch remains schema-attested and code-attested (both prose notes exist and are reachable) but is still not corpus-witnessed on this machine. What would decide it: accept a Write or Edit through the permission dialog after editing the proposed content in that dialog, then read back the resulting toolUseResult - no such acceptance appears in the local corpus."
}
]
},
{
"id": "REC-069",
"area": "record-model",
"behavior": "`memdirStamped` is not the only unschema'd rider. The per-key census over the same window shows `staleRecovered` on 80 of 11421 Edit echoes, likewise spread conditionally (`...we&&{staleRecovered:!0}`) and likewise absent from both schemas. The general statement - the echo is extensible in place with no schema-version marker - is confirmed by two keys, not one.",
"depends": "csift's `TurProbe` deserializes only the small named fields and serde's ignore path skips everything else, so an added key costs nothing and never fails the probe - the tolerance invariant is what absorbs this drift.",
"code": [
{
"path": "src/model/record.rs",
"lines": "300-305",
"snippet": "#[derive(Debug, Default, Deserialize)]\n#[serde(default)]\npub(crate) struct TurProbe {\n pub(crate) r#type: Option<serde_json::Value>,\n #[serde(rename = \"filePath\")]\n pub(crate) file_path: Option<serde_json::Value>,"
}
],
"instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if 'memdirStamped' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if isinstance(t,dict) and 'memdirStamped' in t: k['edit' if 'oldString' in t else 'write']+=1\nprint(k)\nEOF\nCounting rule: one tally per echo carrying a `memdirStamped` key, split by shape. Measured 2026-09-02: write 49, edit 39.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3: over ~/.claude/projects/*/*.jsonl, for every parsed toolUseResult object carrying a `memdirStamped` key, tally 'edit' when `'oldString' in t` else 'write' AND strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -oF '&&{memdirStamped:!0}' | wc -l",
"observed": "corpus: write 49, edit 41. binary: 3 hits for the conditional spread `&&{memdirStamped:!0}`; it appears in the Edit result object `{data:{filePath,oldString,newString,originalFile,structuredPatch,userModified,replaceAll,...we&&{staleRecovered:!0},...Ie&&{memdirStamped:!0},...De&&{gitDiff:De}}}` and in the Write update/create objects, and the key is destructured by both `mapToolResultToToolResultBlockParam` bodies (`let{filePath:r,userModified:o,replaceAll:d,staleRecovered:f,memdirStamped:_}=e`). Neither zod schema (`gJt`, `LRo`) lists it. csift code: src/model/record.rs:299-304 verbatim (`TurProbe` with `#[serde(default)]`).",
"rule": "one tally per echo carrying a `memdirStamped` key over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), split by whether the echo has `oldString`.",
"note": "csift already consumes `staleRecovered` on the recover side, so the tolerance point stands for both: `TurProbe` names six small fields and serde's ignore path drops everything else, so neither rider can fail the probe."
}
]
},
{
"id": "REC-070",
"area": "record-model",
"behavior": "The `filePath` that joins a file mutation to its result lives at the TOP level of the Write/Edit echo but one level DOWN (`file.filePath`) on the Read echo, and the tool_use record itself carries the path under the snake_case `input.file_path`.",
"depends": "csift's `carrier_create_paths` probes only the top-level `filePath`, so a Read echo never mints a carrier row and never sets `is_create`; `recover` has a separate `file`-object arm for the Read shape, and the two must stay separate or a Read would be joined as a mutation.",
"code": [
{
"path": "src/model/mutation.rs",
"lines": "175-179",
"snippet": " /// The carrier side of a file mutation: when this record's `toolUseResult` is an\n /// object carrying a `filePath`, return `(tool_use_id, filePath, is_create)` for\n /// each `tool_result` block, so the `files` joiner can set `is_create` on the\n /// matching structured mutation (and fall back to this `filePath` if the\n /// tool_use's own path was somehow absent)."
},
{
"path": "src/recover/carriers.rs",
"lines": "56-58",
"snippet": " if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n if path_matches(target_file, path.unwrap_or_default()) {"
}
],
"instrument": "The claim asserts three placements but only instruments two. Add the tool_use pass above so the snake_case `input.file_path` third of the claim is measured rather than asserted: Edit 11670 / Read 6122 / Write 2087, zero `filePath` in any input.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "src/model/mutation.rs:175-184 doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3: over ~/.claude/projects/*/*.jsonl, for every parsed toolUseResult object, key by (top-level `filePath` present, `file` object with a `filePath` present) AND a second pass over every `tool_use` block named Write/Edit/MultiEdit/Read/NotebookEdit, keyed by which of `file_path` / `filePath` / `notebook_path` its `input` carries AND strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -F 'type:\"pdf\"'",
"observed": "corpus echoes: (True,False) 13482 write/edit, (False,True) 5805 read, (True,True) 0 - the two placements never co-occur. corpus tool_use inputs: Edit ('file_path',) 11670, Read ('file_path',) 6122, Write ('file_path',) 2087 - snake_case in every case, and no block carried `filePath` in its input. binary: the Read PDF branch returns `{type:\"pdf\",file:{filePath:e,base64:a,originalSize:o}}`, showing the nested placement in the producer. csift code: src/model/mutation.rs:175-179 and src/recover/carriers.rs:56-58 verbatim.",
"rule": "one tally per echo carrying a `filePath` anywhere, keyed by (top-level, nested-under-file), over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist); and one tally per file-tool `tool_use` block, keyed by the path key present in its `input`.",
"note": "The (True,True) cell being empty is the load-bearing part for csift: it is what lets `carrier_create_paths` probe only the top-level `filePath` without ever minting a carrier row from a Read echo."
}
]
},
{
"id": "REC-071",
"area": "record-model",
"behavior": "`toolUseResult.type` is a shared key across unrelated tools with disjoint value sets - `create`/`update` on a Write, `text`/`image`/`file_unchanged`/`parts`/`pdf` on a Read - so `type` alone identifies neither the tool nor a mutation.",
"depends": "csift `files` treats ONLY the literal `create` as a new file and everything else as an edit, which is what keeps a Read's `type:\"text\"` from being read as a mutation kind; widening that comparison to any non-empty `type` would classify reads as writes.",
"code": [
{
"path": "src/model/mutation.rs",
"lines": "186-195",
"snippet": " pub fn carrier_create_paths(&self) -> Vec<(String, String, bool)> {\n let Some(probe) = self.tur_probe() else {\n return Vec::new();\n };\n let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {\n return Vec::new();\n };\n if file_path.is_empty() {\n return Vec::new();\n }"
}
],
"instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if '\"toolUseResult\"' not in line: continue\n try: t=json.loads(line).get('toolUseResult')\n except Exception: continue\n if isinstance(t,dict) and isinstance(t.get('type'),str):\n k[(t['type'], 'filePath' in t, isinstance(t.get('file'),dict))]+=1\nprint(k.most_common(8))\nEOF\nCounting rule: one tally per echo with a string `type`, keyed by `(type, has top-level filePath, has file object)`. Measured 2026-09-02: `('text',False,True)` 5700, `('create',True,False)` 1801, `('image',False,True)` 269, `('update',True,False)` 247, `('file_unchanged',False,True)` 85, `('parts',False,True)` 18, `('pdf',False,True)` 1 - create/update always top-level, the Read values always nested, never both.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "src/model/mutation.rs:181-184 doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3: over ~/.claude/projects/*/*.jsonl, for every parsed toolUseResult with a string `type`, key by (type, top-level `filePath` present, `file` object present) AND strings -n 6 -a <the Claude Code 2.1.258 binary> for the producer literals `type:\"create\"`/`type:\"update\"`/`type:\"file_unchanged\"`/`type:\"parts\"`/`type:\"pdf\"`",
"observed": "corpus: ('text',False,True) 5701, ('create',True,False) 1801, ('image',False,True) 269, ('update',True,False) 247, ('file_unchanged',False,True) 85, ('parts',False,True) 18, ('pdf',False,True) 1 - create/update always top-level-filePath, the five Read values always nested under `file`, never both, and the two value sets never overlap. binary: `type:ee([\"create\",\"update\"])` is the Write schema's enum (1 hit); the Read side has no single enum but the producers are literal, e.g. `{type:\"pdf\",file:{filePath:e,base64:a,originalSize:o}}` (1 hit), `type:\"file_unchanged\"` (2 hits), `type:\"parts\"` (1 hit). csift code: src/model/mutation.rs:186-195 verbatim, and :196 `let is_create = probe.r#type... == Some(\"create\")` is the only comparison made.",
"rule": "one tally per echo with a string `type` over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by (type, has top-level filePath, has file object).",
"note": "The disjointness is the checkable part and it held on every one of the 8122 typed echoes measured: no Read-side value ever appeared with a top-level `filePath`, and neither `create` nor `update` ever appeared with a `file` object. That is what keeps csift's single-literal `create` comparison safe. code-site note: src/model/mutation.rs:181 is stale. The doc comment reads ``/// `toolUseResult.type` in {`create`, `update`, `file_unchanged`, `text`, `image`};`` - five values - but the measured corpus carries seven: `parts` (18) and `pdf` (1) are also Read-side values. The code is unaffected (line 196 compares only against the literal `create`); only the comment under-enumerates."
}
]
},
{
"id": "REC-072",
"area": "record-model",
"behavior": "The per-tool path key diverges the other way on the RESULT side from the `input.file_path` / `input.notebook_path` split on the call side: `Write`, `Edit` and `MultiEdit` echo the path as `toolUseResult.filePath` (binary `responseMembers:[\"filePath\"]`), but `NotebookEdit` echoes it as `toolUseResult.notebook_path` (binary `responseMembers:[\"notebook_path\"]`) inside a ten-member result object {new_source, old_source, cell_type, language, edit_mode, cell_id, error, notebook_path, original_file, updated_file} - there is no `filePath` member on a NotebookEdit carrier at all. The same table shows the split is a per-tool lookup rather than a naming convention: `Read` declares `responseNested:[[\"file\",\"filePath\"],[\"file\",\"outputDir\"]]` instead of `responseMembers`, and `LSP` uses the camelCase `filePath` on its INPUT side.",
"depends": "csift's carrier join `Record::carrier_create_paths` reads ONLY `toolUseResult.filePath`, so a NotebookEdit carrier never joins its tool_use: the notebook mutation's `is_create` stays at its `false` default (\"unknown / treat as edit\") forever, and `files --by file` can never report a notebook as created.",
"code": [
{
"path": "src/model/mutation.rs",
"lines": "190-192",
"snippet": " let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {\n return Vec::new();\n };"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o 'notebook_path:i().describe(\"The path to the notebook file\")[^`]\\{0,200\\}' - expect the NotebookEdit output schema `{notebook_path, original_file, updated_file}` with no `filePath` member. Counting rule: one hit per schema literal.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var loe=\\{.{0,430}' # the per-tool path-key table\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,160}original_file.{0,120}' # the NotebookEdit result schema\ncsift search '' --count-by tool -t agent.tool.use | rg -i 'notebook|multiedit' # on-disk instances",
"observed": "table literal: var loe={Read:{input:\"file_path\",responseNested:[[\"file\",\"filePath\"],[\"file\",\"outputDir\"]]},Write:{input:\"file_path\",responseMembers:[\"filePath\"]},Edit:{input:\"file_path\",responseMembers:[\"filePath\"]},MultiEdit:{input:\"file_path\",responseMembers:[\"filePath\"]},NotebookEdit:{input:\"notebook_path\",responseMembers:[\"notebook_path\"]},Glob:{input:\"path\",responseArrays:[\"filenames\"]},Grep:{input:\"path\",responseArrays:[\"filenames\"]},LSP:{input:\"filePath\",responseMembers:[\"filePath\"]} ; responseMembers:[\"filePath\"] appears 4x and responseMembers:[\"notebook_path\"] 1x in the whole binary ; NotebookEdit result schema declares notebook_path:i().describe(\"The path to the notebook file\"),original_file:i().describe(\"The original notebook content before modification\"),updated_file:i().describe(\"The updated notebook conte... and the runtime object literal is {new_source, old_source, cell_type, language, edit_mode, cell_id, error, notebook_path, original_file, updated_file} ; csift tool census over the whole corpus lists Read 33113, Edit 16970, Write 4889 and NO NotebookEdit and NO MultiEdit key",
"rule": "Binary: one count per distinct `strings` output line containing the literal; the table is a single minified line. Corpus: one count per tool_use BLOCK whose `name` equals the tool, over every *.jsonl under ~/.claude/projects excluding journal.jsonl and elicitations.jsonl.",
"note": "csift code site verified verbatim at the stated line: src/model/mutation.rs:190-192 still reads only `probe.file_path`, and TurProbe declares `#[serde(rename = \"filePath\")]` on that one field (src/model/record.rs:303-304), so the described consequence - a NotebookEdit carrier never joins its tool_use and its `is_create` stays at the `false` default - follows from the current code. It has never been exercised on this corpus because no NotebookEdit call was ever recorded here."
}
]
},
{
"id": "REC-073",
"area": "record-model",
"behavior": "A `tool_result` block marked `is_error:true` never carries a structured `toolUseResult` OBJECT - but the field is not absent either: on 4211 of 4211 errored blocks corpus-wide, in all three lanes, `toolUseResult` is present and typed as a JSON STRING holding the error text ('Error: ...', 'InputValidationError: ...'). The structured object echo is emitted only for ops that landed; a failed op degrades the same field to a string.",
"depends": "csift `recover` derives `has_structured_result` from `rec.tool_use_result.is_some()`, and `Record::tool_use_result` is `Option<Box<serde_json::value::RawValue>>` (src/model/record.rs:123), which is `Some` for a STRING as well as for an object. So on real data a failed op's id IS inserted into `ids_with_result` and the input-side fallback is already suppressed for it there; the separate `failed_ids` set built from `is_error:true` is a redundant second gate on this path rather than the only thing stopping a rejected edit from being replayed. `failed_ids` remains load-bearing as a design guarantee and for the shape the e2e fixture builds (a bare-string tool_result with NO `toolUseResult` key at all), which the live corpus never exhibits.",
"code": [
{
"path": "src/recover/scan.rs",
"lines": "417",
"snippet": " let has_structured_result = rec.tool_use_result.is_some();"
},
{
"path": "src/recover/scan.rs",
"lines": "426-431",
"snippet": " if has_structured_result {\n ids_with_result.insert(id.clone());\n }\n if *is_error == Some(true) {\n failed_ids.insert(id.clone());\n }"
}
],
"instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n for line in open(f,errors='replace'):\n if '\"is_error\":true' not in line: continue\n try: r=json.loads(line)\n except Exception: continue\n c=(r.get('message') or {}).get('content')\n if not isinstance(c,list): continue\n for b in c:\n if isinstance(b,dict) and b.get('type')=='tool_result' and b.get('is_error'):\n k[isinstance(r.get('toolUseResult'),dict)]+=1\nprint(k)\nEOF\nCounting rule: one tally per errored `tool_result` block, keyed by whether its carrying record has an object `toolUseResult`. Measured 2026-09-02: `False` 986, `True` 0.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.6 (`A STRUCTURED op whose tool_result is is_error:true is EXCLUDED`)"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over every *.jsonl under ~/.claude/projects (journal.jsonl and elicitations.jsonl excluded): for every `tool_result` block with is_error true, tally type(record['toolUseResult']).__name__, or 'ABSENT' when the key is missing, bucketed by lane; cross-checked with `csift search '' --count-by result -t agent.tool.result`.",
"observed": "errored tool_result blocks by (lane, toolUseResult type): ('main','str') 986, ('subagent','str') 626, ('workflow','str') 2599 - i.e. 4211 of 4211 carry the key typed STRING, 0 ABSENT, 0 dict. An earlier pass minutes before totalled 4208 with the same 0 ABSENT / 0 dict split, and csift independently reported 4208 at that moment ('210389 ok / 4208 error, 214597 matched record(s) across 2 result key(s)'); the 3-block difference is corpus growth between runs, not a disagreement. The string is the error text - the 12 most common 22-character prefixes are 'Error: Output does not' 754, 'Error: Exit code 1' 469+149+119, 'Error: File does not e' 450, 'Error: File has not be' 218, 'Error: String to repla' 180, 'InputValidationError: ' 102 (387 distinct prefixes overall), never empty. Restricted to failed FILE-TOOL ops the type is still 'str' everywhere: main Edit 251, main Write 45, subagent Edit 23, subagent Write 22, workflow Edit 34, workflow Write 84.",
"rule": "One count per errored `tool_result` BLOCK, keyed by the JSON type of `toolUseResult` on the CARRYING record. Lane = 'main' when the file path has no `/subagents/` component, 'workflow' when it also has `/workflows/`, else 'subagent'. csift's cross-check counts RECORDS, and the two agree, so there is one errored block per record.",
"note": "The in-repo comment at src/recover/scan.rs:410-411 states 'A failed Edit also has NO `toolUseResult` echo, so its id is absent from `ids_with_result`'. The measurement refutes that sentence: the key is present as a string, so the id IS inserted. The behavioural outcome is unchanged (the ghost edit is not replayed either way) - the defect is in the comment's stated mechanism, and it is worth correcting because the comment is what a reader would rely on when deciding whether `failed_ids` can be removed. Both quoted code snippets verify verbatim at src/recover/scan.rs:417 and :426-431."
}
]
},
{
"id": "REC-074",
"area": "record-model",
"behavior": "The result block's `is_error` boolean is written on the `tool_result` BLOCK and never on the record - 125786 blocks carry the key and 0 records do. In workflow lanes the key is present on 72449 of 119879 blocks and true on 2599 (snapshot 2026-09-02 21:52 AEST; the claim's 71339 / 118639 / 2591 was the same shape measured about three hours earlier on the same still-growing corpus).",
"depends": "csift computes `failed_ids` per TURN from that block flag and applies the same gate in `recover`'s replay, in `files`' structured extraction and in the subagent-topology mutation helper, so all three agree that a failed op never landed; dropping the flag would make `files` report writes `recover` can find no history for.",
"code": [
{
"path": "src/files/mutations.rs",
"lines": "23-33",
"snippet": " if let Some(blocks) = rec.blocks() {\n for b in blocks {\n if let crate::model::Block::ToolResult {\n tool_use_id: Some(id),\n is_error: Some(true),\n ..\n } = b\n {\n failed_ids.insert(id.clone());\n }\n }"
}
],
"instrument": "python3 over ~/.claude/projects/**/subagents/workflows/**/*.jsonl tallying, per `tool_result` block, whether the `is_error` key is present and whether it is true. Counting rule: one count per tool_result BLOCK. Observed 118639 blocks, 71339 carrying the key, 2591 true.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.6 (the failed_ids gate); counts measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over every *.jsonl under ~/.claude/projects: for every `tool_result` BLOCK tally (lane, block-count), (lane, 'is_error' key present) and (lane, is_error is True); separately tally records carrying a TOP-LEVEL `is_error` key.",
"observed": "snapshot 2026-09-02 21:52:39 AEST - workflow lane: 119879 tool_result blocks, 72449 carrying the `is_error` key, 2599 true. main lane: 59146 / 33104 / 986. other-subagent lane: 35684 / 20233 / 626. Records with a top-level `is_error` key: 0. Blocks with an `is_error` key: 125786.",
"rule": "One count per `tool_result` BLOCK (a record may carry several). 'key present' means the literal `is_error` key exists on the block regardless of value; 'true' means it equals JSON true. Lane assignment as in REC-073. The corpus is live and grows while sessions run, so the absolute counts are a timestamped snapshot; the reproducible facts are the ratio shape and the block-versus-record placement.",
"note": "Code site verified verbatim: src/files/mutations.rs:23-33 still matches `Block::ToolResult { tool_use_id: Some(id), is_error: Some(true), .. }` on blocks. The claim's structural point - the flag lives on the block - is confirmed decisively by the zero top-level count."
}
]
},
{
"id": "REC-075",
"area": "record-model",
"behavior": "A tool_use and its result are joined only by the `tool_use` block's `id` echoed as the result block's `tool_use_id`, and Claude Code always emits the result inside the same genuine-user turn as the call: 0 of about 214000 joins corpus-wide had a turn-opener candidate between the call record and the result record. Line ORDER within the turn is not guaranteed, however - 4 main-lane joins have the result line 1-2 lines BEFORE the call line, all under CC 2.1.156 / 2.1.177.",
"depends": "csift builds `id_to_path`, `ids_with_result` and `failed_ids` PER TURN and joins the carrier to the structured mutation within the turn, so an id whose result landed in a later turn would be treated as carrier-less: `files` would lose its create-vs-edit accuracy and `recover` would fire the input-side fallback on an op that has an echo.",
"code": [
{
"path": "src/recover/scan.rs",
"lines": "334-399",
"snippet": " for (turn_index, idxs) in turns.iter().enumerate() {\n // tool_use_id → file_path for THIS turn's Read/Edit/Write/MultiEdit tool_uses.\n let mut id_to_path: BTreeMap<String, String> = BTreeMap::new();"
}
],
"instrument": "The claim's instrument - reading the e2e fixture test `tests/cli/recover/modes.rs::recover_subagent_input_fallback_skips_failed_edit` (the function exists at tests/cli/recover/modes.rs:6) - reads a synthetic fixture and so cannot decide a claim about Claude Code's behaviour. Replace it with the corpus join-locality census above, which measures the real transcripts.",
"located": {
"claude_code": null,
"csift": "0.2.0",
"source": "src/recover/scan.rs:410-413 comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over every *.jsonl under ~/.claude/projects: per file, record the line index of every `tool_use` block id and of the first `tool_result` block echoing it as `tool_use_id`, plus the indices of every TURN-OPENER CANDIDATE record; then bucket each join by whether an opener index lies strictly between call and result. Cross-checked corpus-wide with `csift search '' --count-by pairing -t agent.tool`.",
"observed": "join locality over ~214k joins: main same_turn 58489, subagent same_turn 34778, workflow same_turn 119693; opener_between 0 in every lane; orphan_result 0 in every lane; unreturned main 1, subagent 29, workflow 2; main result_before_call 4. 14380 turn-opener candidates were found across 7596 of 7649 files (main 6498, workflow 6746, subagent 1136), so the test was not vacuous; the remaining 53 opener-less files contributed 1322 joins that no boundary could separate and are reported apart. csift agrees: '429040 paired / 35 pending' across 2 pairing keys, with no orphan key at all. The 4 reversed pairs (result line before call line by 1-2 lines) are 2 Edit and 2 Read, all on records stamped version 2.1.156 or 2.1.177, none on a sidechain.",
"rule": "One count per (file, tool_use id) pair whose result appears in the same file. TURN-OPENER CANDIDATE := type=='user' AND message.role=='user' AND not isMeta AND not isCompactSummary AND (content is a string OR content is a block list containing a text block and NO tool_result block) AND the leading text does not start with '<local-command-stdout>', '<command-name>', '<command-message>', '[Request interrupted by user', 'Caveat: The messages below' or '<system-reminder>'. 'opener_between' counts joins where at least one such index is strictly greater than the call index and strictly less than the result index.",
"note": "The reversed-order pairs do not threaten csift: the per-turn maps at src/recover/scan.rs:397-399 are sets keyed by id, built over the whole turn before any event is emitted, so they are order-independent within a turn. The code snippet verifies verbatim at the stated lines. What would additionally strengthen this claim is a lane where a tool call is interrupted and answered after a new human message; none occurs in this corpus."
}
]
},
{
"id": "REC-076",
"area": "record-model",
"behavior": "Claude Code's file-write tool_result TEXT templates are `File created successfully at: <path>` for a create and `The file <path> has been updated successfully.` for an update, both taking the suffix ` (file state is current in your context \\u2014 no need to Read it back)` - the separator is an EM DASH (U+2014), not a hyphen - unless the user modified the content or the write was memdir-stamped; all of those literals live in the 2.1.258 binary.",
"depends": "csift never parses these strings - `recover` routes on the presence or absence of the structured `toolUseResult` object and supplies bytes from the tool_use input instead - so a reworded success message changes nothing; the templates matter only as the observable marker of the carrier-less workflow-subagent shape, where they are the sole trace of a write and carry the path but never the content.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "84-91",
"snippet": " match name.as_str() {\n \"Write\" => {\n if let Some(content) = input.get(\"content\").and_then(serde_json::Value::as_str) {\n events.push(FileEvent {\n line_no,\n turn_index,\n timestamp_utc: ts.clone(),\n kind: EventKind::FullSnapshot {"
},
{
"path": "src/recover/events.rs",
"lines": "36",
"snippet": "/// tool RESULT as a bare `tool_result` string (`\"File created successfully at: …\"`) with"
}
],
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | grep -cF 'File created successfully at' # 2\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | grep -cF 'has been updated' # 5, not 6",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | grep -cF 'File created successfully at'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | grep -cF 'has been updated'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{420}File created successfully at: \\$\\{e\\}'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'O3t=.{0,220}'",
"observed": "'File created successfully at' -> 2 matching strings lines (as expected). 'has been updated' -> 5 matching strings lines, NOT 6. The Write tool's result mapper reads: mapToolResultToToolResultBlockParam({filePath:e,type:n,userModified:r,memdirStamped:o},d){let f=r?\" The user modified your proposed content before accepting it.\":\"\",_=r||o?\"\":O3t;switch(n){case\"create\":return{tool_use_id:d,type:\"tool_result\",content:`File created successfully at: ${e}${f}${_}`};case\"update\":return{tool_use_id:d,type:\"tool_result\",content:`The file ${e} has been updated successfully.${f}${_}`}} and the suffix constant is O3t=\" (file state is current in your context \\u2014 no need to Read it back)\".",
"rule": "`grep -c` counts MATCHING strings OUTPUT LINES, not occurrences; both literals are template fragments the runtime concatenates with the path. The guard is read off the minified ternary: the suffix is appended iff userModified is false AND memdirStamped is false.",
"note": "The guard clause the claim states is exactly right and now has its minified source: `_=r||o?\"\":O3t` with r=userModified and o=memdirStamped. The claim's independence argument also holds - csift never parses these strings; both quoted code sites verify verbatim (src/recover/events.rs:84-91 and the doc line at :36)."
}
]
},
{
"id": "REC-077",
"area": "record-model",
"behavior": "The update-side tool_result STRING is identical for a `Write` of type update and for a successful `Edit` whenever userModified is false - both then render `The file <path> has been updated successfully.` plus the shared suffix - so the result text alone cannot tell a Write from an Edit. The two templates are not byte-identical in general: Write emits `The file ${e} has been updated successfully.${f}${_}` (period before the userModified clause) while Edit emits `The file ${r} has been updated successfully${v}.${C}` (period after it), so under userModified the two texts differ, and Edit alone can substitute a staleRecovered note for the suffix. A `replace_all` Edit renders `The file <path> has been updated. All occurrences were successfully replaced.`",
"depends": "csift's input-side fallback therefore dispatches on the tool_use `name`, never on the result text: `Write` becomes a full-snapshot anchor that RESETS the reconstruction buffer while `Edit` becomes a hunk applied to it, and confusing the two would either wipe or corrupt the replay.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "84-85",
"snippet": " match name.as_str() {\n \"Write\" => {"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o 'has been updated[^\"]\\{0,120\\}' | sort -u - expect the three distinct forms (`has been updated successfully.`, `has been updated successfully${v}.${C}` from the Write path, and `has been updated${v}. All occurrences were successfully replaced.${C}` from the Edit replace_all path). Counting rule: distinct string literals in the binary, deduplicated.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'has been updated[^\"]{0,140}' | sort -u\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{760}All occurrences were successfully replaced'",
"observed": "three template forms, deduplicated: `has been updated successfully.${f}${_}` (Write, case \"update\"), `has been updated successfully${v}.${C}` (Edit, non-replace_all) and `has been updated${v}. All occurrences were successfully replaced.${C}` (Edit, replace_all). The Edit mapper is mapToolResultToToolResultBlockParam(e,n){let{filePath:r,userModified:o,replaceAll:d,staleRecovered:f,memdirStamped:_}=e,v=o?\". The user modified your proposed changes before accepting them. \":\"\",C=f?\" (note: the file had been modified on disk since you last read it \\u2014 the edit applied cleanly, but the file contains other changes not in your context. Read it before edits that depend on surrounding content.)\":o||_?\"\":O3t;if(d)return{...`The file ${r} has been updated${v}. All occurrences were successfully replaced.${C}`};return{...`The file ${r} has been updated successfully${v}.${C}`}",
"rule": "Distinct string literals in the binary, deduplicated with sort -u over `strings` output.",
"note": "The dispatch consequence is unaffected and confirmed: because the ordinary-path texts coincide, csift's input-side fallback must and does dispatch on the tool_use `name` (src/recover/events.rs:84-85, verified verbatim), never on the result text. The Edit mapper also names `staleRecovered` in the same destructure, which is the freshness signal csift's recover consumes."
}
]
},
{
"id": "REC-078",
"area": "record-model",
"behavior": "A TOP-LEVEL session transcript always persists the structured echo on a file write: every one of the 1824 `File created successfully at:` tool_results outside a `subagents/` path carries a dict-valued `toolUseResult`, with zero exceptions - and the stronger form holds too, all 13469 successful main-lane Write and Edit results carry a dict `toolUseResult`, none absent.",
"depends": "This is what makes csift's `ids_with_result` gate safe: in the main lane the fallback can never fire, so main-session reconstruction stays byte-identical to the pre-fallback behaviour and no write is counted twice.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "46-47",
"snippet": "/// Gated on `ids_with_result` so it never double-emits in a top-level session.\npub(crate) fn extract_input_fallback("
}
],
"instrument": "python3 over ~/.claude/projects: for every `File created successfully at:` tool_result, bucket by lane (`/subagents/` in the path or not) and by whether the record has a dict `toolUseResult`. Counting rule: one count per matching tool_result BLOCK. Observed ('main','with_tur') 1824 and ('main','no_tur') 0.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over every *.jsonl under ~/.claude/projects: for every `tool_result` block whose rendered text contains 'File created successfully at:', bucket by lane and by whether the carrying record's `toolUseResult` is a dict; counted twice, once with str.startswith and once with the `in` substring test. A second, sharper pass buckets every SUCCESSFUL Write/Edit result by (lane, tool, type(toolUseResult)).",
"observed": "substring rule: main 1824 with a dict toolUseResult and 0 without - reproducing the claim's 1824 exactly; workflow 1815, all WITHOUT; other-subagent 800 with and 53 without. startswith rule: main 1801 with and 0 without; workflow 1709 without; subagent 797 with, 47 without. Sharper pass over successful Write/Edit results: ('main','Edit','dict') 11421, ('main','Write','dict') 2048, with zero ABSENT in the main lane; ('workflow','Edit','ABSENT') 2666 and ('workflow','Write','ABSENT') 1788, with zero dict; ('subagent','Edit','dict') 1951 vs ABSENT 627 and ('subagent','Write','dict') 841 vs ABSENT 70.",
"rule": "One count per matching `tool_result` BLOCK. 'Rendered text' = the block's content when it is a string, else the '\\n'-join of its text parts. Lane assignment as in REC-073. The two counting rules differ because some results carry the sentence after a prefix, so the rule must be stated: the claim's 1824 is the SUBSTRING rule.",
"note": "The stated safety property is what the measurement supports: in the main lane `ids_with_result` is populated for every write, so the fallback cannot double-emit there. The code site verifies verbatim - the doc line 'Gated on `ids_with_result` so it never double-emits in a top-level session.' is at src/recover/events.rs:46 immediately above `pub(crate) fn extract_input_fallback(` at :47."
}
]
},
{
"id": "REC-079",
"area": "record-model",
"behavior": "Claude Code names the target of a file tool in the tool_use `input` under a per-tool key - `Read`, `Edit`, `Write` and `MultiEdit` all use `input.file_path`, `NotebookEdit` alone uses `input.notebook_path` - and the 2.1.258 binary carries the table literally. The snake-versus-camel split is a per-tool lookup, not a global convention: the same table gives `Read` a `responseNested:[[\"file\",\"filePath\"],[\"file\",\"outputDir\"]]` shape rather than a `responseMembers` list, and gives `LSP` the camelCase `filePath` as its INPUT key.",
"depends": "csift reads the input side in three places - `files`' structured-mutation extractor, `recover`'s `collect_tool_use_paths` (which attributes a later integrity error by `tool_use_id`) and `recover`'s carrier-less input fallback - so a snake/camel drift on either side breaks a different surface, and `NotebookEdit`'s different path key is the one special case each site spells out.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "18-22",
"snippet": " let key = match name.as_str() {\n \"Read\" | \"Edit\" | \"Write\" | \"MultiEdit\" => \"file_path\",\n \"NotebookEdit\" => \"notebook_path\",\n _ => continue,\n };"
},
{
"path": "src/recover/events.rs",
"lines": "99-106",
"snippet": " \"Edit\" => {\n let hunks = vec![EditHunk {\n old_string: input\n .get(\"old_string\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default()\n .to_string(),\n new_string: input"
},
{
"path": "src/model/mutation.rs",
"lines": "146-152",
"snippet": " let (op, key) = match name {\n \"Write\" => (FileOp::Write, \"file_path\"),\n \"Edit\" => (FileOp::Edit, \"file_path\"),\n \"MultiEdit\" => (FileOp::MultiEdit, \"file_path\"),\n \"NotebookEdit\" => (FileOp::NotebookEdit, \"notebook_path\"),\n _ => continue,\n };"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -F 'old_string:i().describe(\"The text to replace\")' | head -1 | cut -c1-600\nExpected: the Edit input schema `tt({file_path:i().describe(\"The absolute path to the file to modify\"),old_string:i()...,new_string:i()...,replace_all:...})`. Counting rule: one `strings` line holds the whole minified constructor; the Write input constructor is found with `grep -F 'The content to write to the file'`.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "src/recover/events.rs:32-44 doc comment; AGENTS.md section 5 files/ bullet + SPEC.md section 6.6 extraction table; table literal measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var loe=\\{.{0,430}'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}old_string:i\\(\\)\\.describe\\(\"The text to replace\"\\).{0,300}'\npython3 over every *.jsonl under ~/.claude/projects tallying tuple(sorted(input.keys())) per file-tool tool_use block",
"observed": "table literal present verbatim: Read:{input:\"file_path\",responseNested:[[\"file\",\"filePath\"],[\"file\",\"outputDir\"]]},Write:{input:\"file_path\",responseMembers:[\"filePath\"]},Edit:{input:\"file_path\",responseMembers:[\"filePath\"]},MultiEdit:{input:\"file_path\",responseMembers:[\"filePath\"]},NotebookEdit:{input:\"notebook_path\",responseMembers:[\"notebook_path\"]} ; Edit input schema: file_path:i().describe(\"The absolute path to the file to modify\"),old_string:i().describe(\"The text to replace\"),new_string:i().describe(\"The text to replace it with (must be different from old_string)\"),replace_all:...,...SCe() ; corpus: 4892 Write blocks and 16971 Edit blocks all key the path as `file_path`, 33116 Read blocks likewise, 0 NotebookEdit and 0 MultiEdit blocks exist.",
"rule": "Binary: the whole table is one minified `strings` line, so one hit per table. Corpus: one count per tool_use BLOCK, keyed by the sorted tuple of its `input` keys.",
"note": "All three code sites verify verbatim at the stated lines: src/recover/events.rs:18-22, src/recover/events.rs:99-106 and src/model/mutation.rs:146-152. The Read arm is worth flagging separately for csift: because Read's result is nested under `file.filePath` rather than a top-level `filePath`, a Read carrier never populates TurProbe.file_path - consistent with the corpus, where 9150 Read results have a dict toolUseResult and none of them carries a top-level `filePath`."
}
]
},
{
"id": "REC-080",
"area": "record-model",
"behavior": "The `Write` tool's input schema is exactly two members, `file_path` (\"The absolute path to the file to write (must be absolute, not relative)\") and `content` (\"The content to write to the file\"), plus a `...SCe()` spread that is empty in 2.1.258. Persisted blocks match: 4889 of 4892 carry exactly those two keys. The 3 exceptions carry a third `description` key and are all stamped userType 'external' on records from CC 2.1.208 and 2.1.220, i.e. calls authored outside the interactive Write tool, not a drift in the tool's own schema.",
"depends": "csift's `recover` input-side fallback reads `input.content` as the FullSnapshot body for a carrier-less Write; if the content key were renamed or the payload moved out of the input, a subagent-authored file would silently reconstruct as empty rather than fail loudly.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "85-86",
"snippet": " \"Write\" => {\n if let Some(content) = input.get(\"content\").and_then(serde_json::Value::as_str) {"
}
],
"instrument": "python3 over ~/.claude/projects: for every `tool_use` block with `name == \"Write\"`, tally `tuple(sorted(input.keys()))`. Counting rule: one count per tool_use BLOCK (not per record, not per file). Observed 4854 Write blocks corpus-wide: 4851 with exactly `('content','file_path')` and 3 with `('content','description','file_path')`.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,220}The content to write to the file.{0,60}'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function SCe.{0,60}'\npython3 over every *.jsonl under ~/.claude/projects tallying tuple(sorted(input.keys())) for every tool_use block named Write, bucketing the outliers by (lane, record version, record userType)",
"observed": "schema literal: var NRo=m(()=>tt({file_path:i().describe(\"The absolute path to the file to write (must be absolute, not relative)\"),content:i().describe(\"The content to write to the file\"),...SCe()})) and the spread resolves to `function SCe(){return{}}` - empty in this build. corpus snapshot 2026-09-02 21:52:39 AEST: 4889 Write blocks with exactly ('content','file_path') and 3 with ('content','description','file_path'); the 3 outliers are (main, version 2.1.208, userType 'external') x2 and (workflow, version 2.1.220, userType 'external') x1.",
"rule": "One count per tool_use BLOCK with name == 'Write' (not per record, not per file), keyed by the sorted tuple of its `input` keys. Outliers additionally keyed by the carrying record's `version` and `userType` fields.",
"note": "csift's fallback reads `input.content` (src/recover/events.rs:85-86, verified verbatim), and an extra `description` key is inert for it, so the outliers do not affect reconstruction. The `...SCe()` spread is worth recording because it is the seam through which a future build could add input members without touching the two named ones."
}
]
},
{
"id": "REC-081",
"area": "record-model",
"behavior": "The `Edit` tool's persisted input is exactly four keys - `file_path`, `old_string`, `new_string`, `replace_all` (plus an empty `...SCe()` spread) - and `replace_all` is ALWAYS materialised even when the model omits it, because the schema coerces `undefined` to `false` (`replace_all:fs((e)=>e===void 0?!1:Ure(e),M().default(!1).optional())`) before the call is persisted: 16971 of 16971 Edit blocks carry it. The binary also carries a second, non-transcript edit surface whose JSON schema lists replace_all as optional and not required, so the 100% materialisation is a property of the interactive tool's schema rather than of every edit entry point.",
"depends": "csift's input-side fallback builds one `EditHunk` from those three payload keys with `.unwrap_or(false)` on `replace_all`; the fallback is the ONLY content source for an Edit in a carrier-less lane, and the defensive `unwrap_or` is never exercised on real data.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "100-105",
"snippet": " let hunks = vec![EditHunk {\n old_string: input\n .get(\"old_string\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default()\n .to_string(),"
}
],
"instrument": "python3 over ~/.claude/projects: tally `tuple(sorted(input.keys()))` for every `tool_use` block with `name == \"Edit\"`. Counting rule: one count per tool_use BLOCK. Observed 16966 Edit blocks, 16966 with exactly `('file_path','new_string','old_string','replace_all')` - i.e. `replace_all` present at 100%.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'replace_all:.{0,160}' | sort -u\npython3 over every *.jsonl under ~/.claude/projects tallying tuple(sorted(input.keys())) for every tool_use block named Edit",
"observed": "the coercion literal is present verbatim: replace_all:fs((e)=>e===void 0?!1:Ure(e),M().default(!1).optional()).describe(\"Replace all occurrences of old_string (default false)\") ; corpus snapshot 2026-09-02 21:52:39 AEST: 16971 Edit blocks, 16971 with exactly ('file_path','new_string','old_string','replace_all') and no other key tuple - replace_all present at 100%. A SEPARATE edit surface in the same binary declares replace_all:{type:\"boolean\"}},required:[\"file_path\",\"old_string\",\"new_string\"] (an 'edit:'-prefixed error namespace), where replace_all is not required.",
"rule": "One count per tool_use BLOCK with name == 'Edit', keyed by the sorted tuple of its `input` keys. Binary: distinct `strings` lines after sort -u.",
"note": "The described consequence holds: the `.unwrap_or_default()` / `unwrap_or(false)` arms in the fallback (src/recover/events.rs:100-105, verified verbatim) are defensive and are never exercised by real data, since every recorded Edit carries all three payload keys and the flag."
}
]
},
{
"id": "REC-082",
"area": "record-model",
"behavior": "`NotebookEdit`'s input is `{notebook_path, cell_id?, new_source, cell_type?, edit_mode?}` with `edit_mode` an enum of `[\"replace\",\"insert\",\"delete\"]` defaulting to replace - the cell body rides `new_source`, not `content` and not `new_string` - and the local corpus contains ZERO NotebookEdit tool_use blocks.",
"depends": "csift's `recover` input-side fallback has arms for `Write`/`Edit`/`MultiEdit` only and reads `input.file_path` unconditionally, so a notebook edited in a carrier-less (workflow-agent) lane is invisible to `recover` even though `files` still attributes it via the `notebook_path` table.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "77-83",
"snippet": " let path = input\n .get(\"file_path\")\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default();\n if !path_matches(target_file, path) {\n continue;\n }"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o 'notebook_path:i().describe(\"The absolute path to the Jupyter notebook[^`]\\{0,400\\}' for the schema; then python3 over ~/.claude/projects counting tool_use blocks with `name == \"NotebookEdit\"` (counting rule: one per block; observed 0).",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a <claude-code-2.1.258-binary> | grep -o 'notebook_path:i().describe(\"The absolute path to the Jupyter notebook[^`]\\{0,700\\}' | head -1 AND csift search '' --count-by tool --max-count 0 AND sed -n '77,83p' src/recover/events.rs ; sed -n '10,22p' src/recover/events.rs ; grep -n notebook_path src/model/mutation.rs",
"observed": "Binary, one schema literal: notebook_path:i().describe(\"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)\"),cell_id:i().optional().describe(\"The ID of the cell to edit. ...\"),new_source:i().describe(\"The new source for the cell\"),cell_type:ee([\"code\",\"markdown\"]).optional().describe(\"...If using edit_mode=insert, this is required.\"),edit_mode:ee([\"replace\",\"insert\",\"delete\"]).optional().describe(\"The type of edit to make (replace, insert, delete). Defaults to replace.\") -- Corpus: the tool census emitted 35 tool keys (Bash 245899, Read 66265, Edit 33941, WebFetch 20008, Write 9762, ...) over 666154 matched records; NotebookEdit is not among them. Code: src/recover/events.rs:77-83 matches verbatim (the input-side fallback reads only input.file_path); the recover path table at events.rs:19-20 does carry \"NotebookEdit\" => \"notebook_path\", and src/model/mutation.rs:150 reads \"NotebookEdit\" => (FileOp::NotebookEdit, \"notebook_path\") for the files attribution.",
"rule": "One hit per schema literal in the binary. The csift census counts each matched record once per tool key across every transcript under ~/.claude/projects (16 project dirs, 7827 jsonl files, 5.2 GB); a tool name absent from the key list means zero tool_use/tool_result blocks carrying it.",
"note": "Both halves reproduce. The schema field set, the edit_mode enum and its documented replace default are byte-for-byte as claimed, and the corpus still contains no NotebookEdit call, so the recover blind spot the claim describes stays theoretical here rather than observed."
}
]
},
{
"id": "REC-083",
"area": "record-model",
"behavior": "The written payload of a file tool lives under a different input key per tool: `Write.content`, `Edit.new_string` (and `MultiEdit.edits[].new_string`), `NotebookEdit.new_source` - there is no common content key.",
"depends": "`csift wait --until 'write:<path-regex>[:<line-regex>]'` scans exactly `content`, `new_string` and `new_source` as string-valued input keys, so a MultiEdit's per-hunk `edits[].new_string` is unreachable to the line regex and such a write can only ever satisfy the path half of the condition.",
"code": [
{
"path": "src/live/conditions.rs",
"lines": "172-182",
"snippet": " } if matches!(n.as_str(), \"Write\" | \"Edit\" | \"MultiEdit\" | \"NotebookEdit\") => {\n let path = input\n .get(\"file_path\")\n .or_else(|| input.get(\"notebook_path\"))\n .and_then(serde_json::Value::as_str)\n .unwrap_or_default();\n if !path_re.is_match(path) {\n return false;\n }\n line_re.as_ref().is_none_or(|re| {\n [\"content\", \"new_string\", \"new_source\"].iter().any(|k| {"
}
],
"instrument": "Read the unit test at src/live/tests/conditions.rs:143-145, which fixtures a `NotebookEdit` tool_use with `notebook_path` plus `new_source` and asserts the path source is consulted and a path miss short-circuits before the line regex.",
"located": {
"claude_code": null,
"csift": "0.9.0",
"source": "src/live/conditions.rs:24-26 doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a <claude-code-2.1.258-binary> > STRINGS ; grep -o 'content:i().describe(\"The content to write[^`]\\{0,120\\}' STRINGS ; grep -o 'new_string:i().describe(\"The text to replace it with[^`]\\{0,90\\}' STRINGS ; grep -o 'edits:\\[{old_string:[^)]\\{0,90\\}' STRINGS ; grep -o 'new_source:i().describe(\"The new source for the cell\"' STRINGS AND sed -n '155,181p' src/live/conditions.rs",
"observed": "Write: content:i().describe(\"The content to write to the file\"),... Edit: new_string:i().describe(\"The text to replace it with (must be different from old_string)\") MultiEdit hunks: edits:[{old_string:r,new_string:o,replace_all:d}] NotebookEdit: new_source:i().describe(\"The new source for the cell\"). Four different keys, no shared one. csift src/live/conditions.rs:161-171 matches the claimed snippet verbatim and the key list at line 171 is exactly [\"content\", \"new_string\", \"new_source\"], read off input at the TOP level with no descent into edits[].",
"rule": "One hit per schema literal in the binary; four literals, four distinct payload keys. The csift key array is read verbatim from the current file at the claimed lines.",
"note": "The binary confirms all four payload keys including the nested MultiEdit hunk shape, and the csift line-regex key list is unchanged. The consequence the claim draws is currently unobservable on this machine: the corpus holds zero MultiEdit calls (REC-084), so no real record exercises the edits[] gap."
}
]
},
{
"id": "REC-084",
"area": "record-model",
"behavior": "`MultiEdit` and `NotebookEdit` are still registered tool names in the 2.1.258 binary (both appear in the tool registry array and in the file-editing set `[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]`) but produce ZERO tool CALLS across the entire local corpus, so their result-echo shapes are unverified by observation - real file-tool traffic is Read/Edit/Write only.",
"depends": "csift models both (`FileOp::MultiEdit`/`FileOp::NotebookEdit`, the `edits[]` input fallback, the `notebook_path` key) on schema grounds alone; a drift in either echo would go undetected until such a call appears, and `recover` has no carrier-side MultiEdit arm at all - it relies on the Edit arm's `oldString` shape.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "127-133",
"snippet": " \"MultiEdit\" => {\n let hunks: Vec<EditHunk> = input\n .get(\"edits\")\n .and_then(serde_json::Value::as_array)\n .map(|arr| {\n arr.iter()\n .map(|e| EditHunk {"
},
{
"path": "src/model/mutation.rs",
"lines": "16-24",
"snippet": "pub enum FileOp {\n /// `Write` tool - writes a file whole (a create when the path was new).\n Write,\n /// `Edit` tool - a single in-place string replacement in an existing file.\n Edit,\n /// `NotebookEdit` tool - edits a Jupyter notebook cell (`notebook_path`).\n NotebookEdit,\n /// `MultiEdit` tool - multiple edits to one file in a single call.\n MultiEdit,"
},
{
"path": "src/recover/events.rs",
"lines": "127-131",
"snippet": " \"MultiEdit\" => {\n let hunks: Vec<EditHunk> = input\n .get(\"edits\")\n .and_then(serde_json::Value::as_array)\n .map(|arr| {"
}
],
"instrument": "csift search '' --count-by tool --max-count 0 over every project, plus a direct name search. Counting rule: the census counts each matched RECORD once per tool key across every transcript. Measured 2026-09-02 at 21:5x local: Read 66265, Edit 33941, Write 9762, and NEITHER MultiEdit NOR NotebookEdit appears as a key at all (0 blocks). A bare csift search '\"name\":\"MultiEdit\"' --count-only returns 19 and '\"name\":\"NotebookEdit\"' returns 20, but those are prose mentions in transcripts, not tool calls - which is exactly why the tool census is the right instrument. Cross-check the names are still shipped: strings -a <binary> | grep -c MultiEdit returns 10 and grep -c NotebookEdit returns 15.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' --count-by tool --max-count 0 AND csift search '\"name\":\"MultiEdit\"' --count-only AND csift search '\"name\":\"NotebookEdit\"' --count-only AND strings -a <claude-code-2.1.258-binary> | grep -c MultiEdit ; strings -a <claude-code-2.1.258-binary> | grep -c NotebookEdit AND sed -n '127,133p' src/recover/events.rs ; sed -n '16,24p' src/model/mutation.rs ; grep -n MultiEdit src/recover/*.rs",
"observed": "Census 2026-09-02, 35 tool keys: Bash 245899, Read 66265, Edit 33941, WebFetch 20008, WebSearch 19683, Write 9762, ...; MultiEdit and NotebookEdit appear as no key at all. Literal-name searches: '\"name\":\"MultiEdit\"' 19 records, '\"name\":\"NotebookEdit\"' 20 records - prose mentions, not calls. Binary: grep -c MultiEdit = 10 lines, grep -c NotebookEdit = 15 lines. Code: src/recover/events.rs:127-133 and src/model/mutation.rs:16-24 match verbatim; src/recover/carriers.rs:92 reads '// (3) Edit result: {filePath, oldString, newString, structuredPatch, ...} (no type)' and grep finds no MultiEdit arm anywhere in src/recover/carriers.rs.",
"rule": "The census counts each matched record once per tool key over every transcript under ~/.claude/projects; a name that is not a key has zero blocks. --count-only counts records whose raw bytes match the pattern, so it includes prose. Binary counts are grep -c (LINES containing the token), matching the claim's stated form.",
"note": "The behavior stands: both names are registered and neither is ever called. Only the recorded counts needed correcting - the corpus grew between the original measurement and this re-run, so Read moved 66151 to 66265, Edit 33935 to 33941 and Write 9718 to 9762, while the two zero counts and both binary counts reproduced exactly. Two housekeeping notes: the claim's third code entry (src/recover/events.rs:127-131) is a narrower duplicate of its first, and the carrier-side gap it asserts is confirmed positively - the recover carrier module names no MultiEdit arm and keys the Edit result purely on the presence of oldString/newString with no type field."
}
]
},
{
"id": "REC-085",
"area": "record-model",
"behavior": "The file-tool names Claude Code writes into `message.content[].name` are the literal strings `Read`, `Write`, `Edit`, `MultiEdit` and `NotebookEdit`; the binary keeps them as one closed file-editing set (`[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]`) alongside the full 2.1.258 tool registry.",
"depends": "csift's `files` byte prefilter never mentions MultiEdit or NotebookEdit: it relies on `Edit` and `Write` being SUBSTRINGS of every file-editing tool name, so a future tool that mutates files without carrying `Edit`/`Write`/`Bash`/`filePath` in its line would be dropped before the parse and never appear in any `files` rollup.",
"code": [
{
"path": "src/files/run.rs",
"lines": "183-186",
"snippet": " memmem::Finder::new(b\"Edit\"),\n memmem::Finder::new(b\"Write\"),\n memmem::Finder::new(b\"Bash\"),\n memmem::Finder::new(b\"filePath\"),"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o '\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\]' - expect the closed file-editing set literal. Counting rule: one hit per array literal in the binary.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a <claude-code-2.1.258-binary> | grep -o '\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\]' | wc -l AND strings -a <claude-code-2.1.258-binary> | grep -o '\"Bash\"[^`]\\{0,300\\}' | grep -i notebook AND sed -n '175,195p' src/files/run.rs",
"observed": "The closed file-editing set literal [\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"] occurs exactly 1 time. The tool registry array reads \"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",\"Edit\",\"MultiEdit\",\"Write\",\"NotebookEdit\",\"Glob\",\"Grep\",\"LS\",\"TodoWrite\",\"TaskCreate\",\"TaskGet\",\"TaskList\",\"TaskUpdate\",\"TaskStop\",\"TaskOutput\",\"LSP\",\"ReadMcpResourceTool\",... and a second built-in-name array reads \"Bash\",\"Read\",\"Write\",\"Edit\",\"Glob\",\"Grep\",\"NotebookEdit\",\"WebFetch\",\"WebSearch\",\"Task\",\"TodoWrite\",... csift src/files/run.rs:183-186 matches the claimed snippet verbatim.",
"rule": "One hit per array literal in the binary. The csift prefilter needles are read verbatim from the current file.",
"note": "Confirmed, with one detail worth recording: the files prefilter array is now SIX needles, not the four the claim quotes - b\"Edit\", b\"Write\", b\"Bash\", b\"filePath\", b\"file-history-snapshot\" and b\"is_error\". The quoted four are still verbatim at the claimed lines and neither MultiEdit nor NotebookEdit is among the six, so the substring-dependence the claim warns about is intact."
}
]
},
{
"id": "REC-086",
"area": "record-model",
"behavior": "Claude Code labels an `Edit` whose `old_string` is the EMPTY string as a `Create` in its own UI (`if(e.old_string===\"\")return\"Create\"`), yet that op still emits the Edit-shaped result with no `type` key.",
"depends": "csift `files` derives create-vs-edit ONLY from `toolUseResult.type == \"create\"`, so an empty-old_string Edit is reported `edit` with `is_create:false`; csift `recover` refuses the same op outright, because `apply_string_edit` returns `UnAnchorable` on an empty `old_string`.",
"code": [
{
"path": "src/recover/buffer.rs",
"lines": "306-313",
"snippet": " for h in hunks {\n if h.old_string.is_empty() || !text.contains(&h.old_string) {\n return EditOutcome::UnAnchorable;\n }\n if h.replace_all {\n text = text.replace(&h.old_string, &h.new_string);\n } else {\n text = text.replacen(&h.old_string, &h.new_string, 1);"
},
{
"path": "src/model/mutation.rs",
"lines": "186-193",
"snippet": " pub fn carrier_create_paths(&self) -> Vec<(String, String, bool)> {\n let Some(probe) = self.tur_probe() else {\n return Vec::new();\n };\n let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {\n return Vec::new();\n };\n if file_path.is_empty() {"
}
],
"instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -F 'if(e.old_string===\"\")return\"Create\"' | wc -l # expect 1\nCounting rule: one `strings` output line contains the whole label function; read the surrounding `function cxe(e){...}` to confirm it is the Edit tool's display-label helper, not a result-shape branch.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -a <claude-code-2.1.258-binary> | grep -cF 'if(e.old_string===\"\"' AND strings -a <claude-code-2.1.258-binary> | grep -F 'if(e.old_string===\"\"' AND csift search '\"old_string\": *\"\"' --count-only ; csift search '\"oldString\":\"\"' --count-only AND sed -n '306,313p' src/recover/buffer.rs ; sed -n '180,215p' src/model/mutation.rs",
"observed": "Exactly 1 matching strings line. The whole label helper is function cxe(e){if(!e)return\"Update\";if(e.file_path?.startsWith(Ea()))return\"Updated plan\";if(e.edits!=null)return\"Update\";if(e.old_string===\"\")return\"Create\";return\"Update\"}. On the same strings line the Edit result-echo schema is c({filePath:i(),oldString:i(),newString:i(),originalFile:i().nullable(),structuredPatch:R(...),userModified:M(),replaceAll:M(),gitDiff:...}) - it carries NO type key, while the Write result schema on another line is c({type:ee([\"create\",\"update\"]).describe(\"Whether a new file was created or an exis...\"). Corpus: csift search for an empty old_string returns 0 records and for an empty oldString 0 records. Code: src/recover/buffer.rs:306-313 and src/model/mutation.rs:186-193 match verbatim, and mutation.rs:196 is the single create test - probe.r#type ... == Some(\"create\").",
"rule": "One strings line contains the whole label function, so grep -cF counts it once. The corpus figures count RECORDS whose raw bytes contain the literal; 0 means no transcript in ~/.claude/projects records an Edit with an empty old_string.",
"note": "Both halves of the mechanism reproduce from the binary: the Create label branch is present exactly once, and the Edit result echo genuinely has no type field while the Write echo does - which is precisely why csift can only ever report such an op as an edit. Two additions worth carrying: the same helper has three other branches (a plan-directory path returns \"Updated plan\", and a call carrying edits returns \"Update\", so MultiEdit is never labelled Create), and the corpus contains zero empty-old_string Edits, so the csift consequence is derived rather than witnessed here."
}
]
},
{
"id": "REC-087",
"area": "record-model",
"behavior": "Claude Code writes a transcript by APPENDING whole lines and never rewrites, reorders, splits or deletes an already-written line: across two windows (20 s and 25 s) over the three most recently modified transcripts, every previously-read byte prefix re-hashed identically (6 of 6 observations) and no file ever shrank, while 2 of the 3 files in the 25 s window grew during it. The binary's file writers are appendFileSync (24 occurrences on 15 strings lines) with no in-place transcript rewrite path.",
"depends": "Every `Lnnnn` header, every `csift show @<id> --line N` refetch, the show/verbatim continuation pointers and `recover --file-lines` treat a physical line number as a durable address; if a line were ever rewritten in place, every printed refetch would silently fetch a different record than the one that was matched.",
"code": [
{
"path": "src/parse/lines.rs",
"lines": "18-21",
"snippet": "/// Open + memory-map a file read-only. Length is captured at open; a concurrently\n/// growing/shrinking live session is tolerated (we treat the length as fixed and\n/// skip any torn trailing fragment). Returns `Ok(None)` for an empty file.\npub(crate) fn mmap_file(path: &Path) -> Result<Option<Mmap>> {"
}
],
"instrument": "Python: take the 3 newest `~/.claude/projects/*/*.jsonl` by mtime, record `(size_n, md5(first size_n bytes))`, `time.sleep(20)`, re-read the first `size_n` bytes and re-hash. Expect prefix hashes byte-identical for every file and at least one file's size strictly larger. Counting rule: one observation per file; a prefix hash mismatch falsifies the claim.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3: walk ~/.claude/projects for every *.jsonl, sort by mtime descending, take the 3 newest; record (size_n, md5 of the first size_n bytes); time.sleep(25); re-read the first size_n bytes and re-hash. AND strings -a <claude-code-2.1.258-binary> | grep -o appendFileSync | wc -l ; strings -a <claude-code-2.1.258-binary> | grep -c appendFileSync AND sed -n '18,21p' src/parse/lines.rs",
"observed": "Run over the 3 newest transcripts: prefix md5 identical 3/3, size strictly grew 2/3 (201338->220620 and 140305->147933 bytes), shrank 0/3. An earlier 20 s run restricted to top-level transcripts: prefix md5 identical 3/3, grew 0/3, shrank 0/3. Binary: appendFileSync occurs 24 times on 15 distinct strings lines. csift src/parse/lines.rs:18-21 matches verbatim.",
"rule": "One observation per file. A prefix-hash mismatch or a size decrease falsifies append-only; the number of files that GREW in the window depends on which sessions happen to be writing and is not a property of the format. Binary count: grep -o | wc -l counts occurrences (24); grep -c counts lines containing the token (15).",
"note": "The append-only property reproduces and is if anything stronger than recorded, but two numbers needed fixing. The claim's 'one of the three grew' is a property of that particular window, not of the format - a re-run saw two of three grow and an earlier run saw none - so the behavior is restated with the growth count named as window-dependent. And the appendFileSync figure needs its counting rule attached: 24 is the occurrence count, while a stranger running the more natural grep -c would get 15 and wrongly read it as drift. An independent corroboration came from the compaction census in REC-088: one transcript carries 80 compaction boundaries and is still 123044 lines long, so 80 compaction events truncated nothing."
}
]
},
{
"id": "REC-088",
"area": "record-model",
"behavior": "A compaction does NOT rewrite the transcript: Claude Code appends a `type:\"system\"` `subtype:\"compact_boundary\"` record and a separate `type:\"user\"` `isCompactSummary:true` record after the existing lines, so every pre-compaction line number stays valid.",
"depends": "This is what lets `verbatim` reconstruct the clipped turns from the SAME file and lets a refetch printed before a compaction still resolve afterwards; if compaction rewrote the file, every stored `Lnnnn` address in a prior answer would go stale.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "186-188",
"snippet": "pub(crate) fn clone_origin(path: &Path, boundary_uuid: &str) -> Option<String> {\n let dir = path.parent()?;\n let finder = memchr::memmem::Finder::new(boundary_uuid.as_bytes());"
}
],
"instrument": "`csift search '' @<id> -t harness.compaction.boundary --format json` then `csift show @<id> --line <boundary line - 1>`: the record immediately before the boundary must be a pre-compaction record, proving the boundary was appended rather than inserted. Counting rule: one boundary per compaction event.",
"located": {
"claude_code": null,
"csift": "0.9.4",
"source": "AGENTS.md section 3.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search '' @<id> -t harness.compaction.boundary --format json --max-count 0 AND csift show @<id> --line 1533..1538 --format json AND python3 over that one transcript: count lines with type==system and subtype==compact_boundary, count lines with type==user and isCompactSummary truthy, test whether each boundary line b is followed at b+1 by a summary, and resolve each boundary's logicalParentUuid to the line that first carries that uuid. AND sed -n '186,188p' src/session/summarize.rs",
"observed": "In one top-level transcript of 123044 lines: 80 records with type==system and subtype==compact_boundary at strictly increasing lines 1537 ... 122592; 80 records with type==user and isCompactSummary true at lines 1538 ... 122593; 80 of 80 boundaries are followed at line b+1 by the summary. The nearest timestamped record before the first boundary is line 1533, type user, ts 2026-05-25T14:50:11.469Z, versus the boundary's 2026-05-25T14:52:40.365Z. For the first 10 boundaries, logicalParentUuid resolved to a STRICTLY EARLIER line in the same file 7 times (lines 1537->1533, 2801->2792, 3912->3899, 5801->5784, 7015->7002, 7877->7871, 10777->10761); every boundary's own parentUuid is null. csift show renders line 1537 as harness.compaction.boundary and 1538 as harness.compaction.summary. csift src/session/summarize.rs:186-188 matches verbatim.",
"rule": "One boundary per compaction event. A boundary at line b is 'appended' if lines below b still hold pre-compaction records and b is greater than the previous boundary; a rewrite would show as a truncated file or a boundary at line 1. The logicalParentUuid resolution maps a uuid to the FIRST line carrying it.",
"note": "Decisive. Eighty compactions in one transcript left it 123044 lines long with every boundary at a higher line number than the last, each immediately followed by its summary, and the record just below the first boundary is an ordinary user message stamped two and a half minutes earlier. The back-link is real too: seven of the first ten boundaries name a logicalParentUuid that resolves to a strictly earlier line in the same file. The other three resolve to no line in this file at all, which is consistent with a re-anchor onto a record that an earlier compaction lineage had already clipped - worth a follow-up but not a counterexample to appending."
}
]
},
{
"id": "REC-089",
"area": "record-model",
"behavior": "Record timestamp values are NOT monotone in file order: over the 14 newest-by-mtime top-level transcripts sized 200 KB to 40 MB (38 files are in that band), counting only the top-level timestamp of type user/assistant records and calling a backstep any record more than 1 ms earlier than the running maximum of its predecessors, there were 90 backsteps over 10906+ (measured 14508) records (0.62%), present in 7 of the 14 files, 59 of them larger than 60 s, worst 52904 s (about 14.7 hours).",
"depends": "`list` derives a session's first activity from a HEAD read and its last from a TAIL read (first record = earliest, last = newest), and the all-projects flood-guard cap keeps the most-recently-active rows by that tail value - so a session whose tail carries a replayed or clone-copied older timestamp can be capped away or mis-sorted; `stats` is the surface that gets it right by taking a min/max over every timestamped record.",
"code": [
{
"path": "src/stats.rs",
"lines": "240-247",
"snippet": " if let Some(ts) = rec.timestamp.as_deref() {\n if out.first_utc.as_deref().is_none_or(|f| ts < f) {\n out.first_utc = Some(ts.to_string());\n }\n if out.last_utc.as_deref().is_none_or(|l| ts > l) {\n out.last_utc = Some(ts.to_string());\n }\n }"
},
{
"path": "src/session/summarize.rs",
"lines": "20-21",
"snippet": " let (head_skipped, head_consumed) =\n head_records_prefiltered(path, line_is_list_candidate, |rec| {"
}
],
"instrument": "python3 over a DETERMINISTIC sample of ~/.claude/projects/*/*.jsonl: keep files sized 200 KB to 40 MB, sort by mtime descending, take 14. Per file walk lines in order, parse, keep records with type in {user, assistant} and a parseable ISO timestamp, and count a backstep whenever t < running_max - 0.001. Report backsteps, records, files-with-any, count over 60 s and the worst delta. Counting rule: one observation per qualifying record; ties and sub-millisecond jitter are not backsteps.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3: from ~/.claude/projects/*/*.jsonl keep files whose size is between 200 KB and 40 MB, sort by mtime descending, take the first 14; per file walk lines in order, parse, keep records whose type is user or assistant with a parseable ISO timestamp, and count a backstep whenever t < running_max - 0.001 s. AND sed -n '240,247p' src/stats.rs ; sed -n '20,21p' src/session/summarize.rs",
"observed": "38 files sit in the size band; the 14 newest by mtime were sampled. 90 backsteps over 14508 user/assistant timestamped records = 0.62 percent, present in 7 of the 14 files, 59 of them larger than 60 s, worst delta 52904.2 s (14.7 hours). Per-file backstep counts: 30, 21, 15, 10, 10, 2, 2 and seven files with none. csift src/stats.rs:240-247 and src/session/summarize.rs:20-21 match verbatim.",
"rule": "One observation per qualifying record. Ties and sub-millisecond jitter are not backsteps. Sample selection is deterministic and rerunnable: size band 200 KB to 40 MB, newest 14 by mtime, top-level transcripts only.",
"note": "The phenomenon reproduces at the same order of magnitude but every number moved, because the original sample was drawn randomly with no recorded seed and cannot be rerun. The sampling rule is replaced with a deterministic one (newest 14 by mtime inside the same size band) and the figures are restated from it: 90 backsteps in 14508 records over 7 of 14 files, worst 14.7 hours rather than 10.3. The mechanism is now identified rather than guessed - the worst backstep sits exactly at the start of a whole-history replay block, the same resume replay characterised under REC-091, so the large deltas are replayed old records rather than clock skew."
}
]
},
{
"id": "REC-090",
"area": "record-model",
"behavior": "Because timestamps are not monotone in file order, Claude Code's file order and its time order are two DIFFERENT orderings of the same records - and csift's own windows split across them: `--turn` and `--line` window on file order while `--since`/`--until` and `agents --order-by` window on timestamps.",
"depends": "A query that mixes the two (a `--turn` range plus a `--since`) can return a set that is neither a contiguous file range nor a contiguous time range, and `agents --order-by completion` sorts on a named instant that may precede an earlier-in-file sibling's instant.",
"code": [
{
"path": "src/agents/run.rs",
"lines": "68",
"snippet": " nodes.retain(|n| window_admits(n, &time_window, args.order_by));"
}
],
"instrument": "On a file measured to carry a backstep, run `csift search '' @<id> --turn -5.. --format json` and compare the emitted `ts_utc` sequence against sorted order; a non-sorted sequence in a file-order window is the observation. Counting rule: one comparison per emitted record pair.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/agents/run.rs:68"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift show @<id> --turn 15 --format json --max-count 0 AND csift show @<id> --line 4804..4812 --format json, on a transcript first measured to carry a backstep at line 4808 with a 168.9 s delta; compare the emitted ts_utc sequence against its own sorted order. AND sed -n '68p' src/agents/run.rs",
"observed": "--turn 15: 6 timestamped records spanning lines 4799-4810, file order does NOT equal time order, 2 adjacent descents and 5 pairwise inversions. --line 4804..4812: 7 timestamped records, 9 pairwise inversions; the rendered sequence is line 4806 at 14:06:13.786Z, 4807 at 14:06:12.863Z, 4808 at 14:03:23.968Z, 4809 at 14:03:23.968Z, 4810 at 14:06:13.885Z, 4811 at 14:06:12.868Z, 4812 at 14:06:13.355Z. csift src/agents/run.rs:68 matches verbatim.",
"rule": "One comparison per emitted record pair. An inversion is a pair (i,j) with i before j in file order and ts_utc[i] greater than ts_utc[j]. A file-order window whose emitted timestamps are not sorted is the observation.",
"note": "Confirmed by csift's own output, using the instrument the claim names. A single turn window - one turn, not a range - already emits six records whose timestamps run forward, back nearly three minutes, then forward again, so a file-order window is demonstrably not a time-order window. The inversions here are entirely inside one turn, which sharpens the claim: the two orderings can disagree without any turn boundary being crossed."
}
]
},
{
"id": "REC-091",
"area": "record-model",
"behavior": "A record's top-level uuid is NOT a primary key: over the 14 newest-by-mtime top-level transcripts sized 200 KB to 40 MB, counting lines that carry a top-level uuid and summing (occurrences - 1) per repeated value, 4 of the 14 files carried repeats totalling 1857 extra lines over 47696 uuid-bearing lines (3.9%). The dominant within-file cause is a RESUME REPLAY, not the compaction re-anchor: after an ai-title / mode / permission-mode line triple, Claude Code re-appends the whole prior history into the SAME file, and the replayed copy commonly differs from the original only by an added top-level slug key (844 of 1856 pairs are exactly original-plus-slug; only 49 of 1857 pairs, 2.6%, straddle a compact_boundary). Across files, a clone byte-copies records and shares uuids with its origin (measured: 558 shared uuids, 13.0% of the clone's distinct set); a subagent transcript does NOT - 0 of 1876 parent-child transcript pairs share a single top-level uuid.",
"depends": "`search`'s `AddressSet` holds a SET of uuids and admits every record whose own uuid is in it, so all copies surface; any future join written as \"find THE record with this uuid\" would silently pick one of several, and the clone-origin probe already has to verify the uuid CARRIER rather than trust the uuid alone.",
"code": [
{
"path": "src/search/scan.rs",
"lines": "20-21",
"snippet": " let address = AddressSet { lines, uuids };\n let use_address = !(address.lines.is_empty() && address.uuids.is_empty());"
},
{
"path": "src/model/record.rs",
"lines": "109",
"snippet": " pub logical_parent_uuid: Option<String>,"
}
],
"instrument": "python3 over a deterministic sample (files 200 KB to 40 MB, newest 14 by mtime): per file, Counter every top-level uuid string, then sum c-1 for every value with c>1. Report extras, total uuid-bearing lines and the number of files with any repeat. Then classify each repeat pair by whether a compact_boundary line lies strictly between the occurrences and by the key-set delta of the two raw lines. Counting rule: repeats counted per file, so cross-file duplication is NOT included in the number.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 over the same deterministic sample (files 200 KB to 40 MB, newest 14 by mtime): per file Counter every top-level uuid string, then sum c-1 for every value with c>1. AND per file, test for each repeat pair whether a compact_boundary line lies strictly between the two occurrences. AND per file, compare the two raw lines of each repeat pair byte-wise and by key set. AND a parent-versus-child pass: for every session dir with a subagents/ dir and a parent transcript under 30 MB, intersect the parent's uuid set with each child transcript's. AND csift list --max-count 0 --format json to find a row carrying clone_of, then intersect the clone's and origin's uuid sets. AND sed -n '20,21p' src/search/scan.rs ; sed -n '108p' src/model/record.rs",
"observed": "Sample: 1857 extra uuid lines over 47696 uuid-bearing lines = 3.89 percent, in 4 of the 14 files (per-file extras 613, 615, 588, 41). Boundary test: only 49 of 1857 repeat pairs (2.6 percent) straddle a compact_boundary line - 7/41, 42/615, 0/613 and 0/588. Shape test on the 21.7 MB file: 613 repeat pairs, first occurrences spanning lines 25-3996 and second occurrences 4010-4626, immediately preceded by an ai-title / mode / permission-mode line triple at 4007-4009; 0 of the first 200 pairs are byte-identical and every one of those 200 differs from its original by exactly one added top-level key, slug. Across the four files, 844 of 1856 pairs are exactly first-copy-plus-slug and 124 are byte-identical. Parent-child pass: 16 session dirs, 1876 parent-child transcript pairs, 0 pairs sharing even one top-level uuid. Clone pass: exactly 1 row in the corpus carries clone_of; the 47.9 MB clone has 4293 distinct uuids and its 720.9 MB origin 70729, and 558 uuids (13.0 percent of the clone's) appear in BOTH files. csift src/search/scan.rs:20-21 and src/model/record.rs:108 match verbatim.",
"rule": "Repeats counted per file, so cross-file duplication is excluded from the 1857/47696 figure. A repeat pair 'straddles' a boundary when a compact_boundary line number lies strictly between the two occurrence line numbers. Cross-file sharing is a set intersection of top-level uuid strings between two whole files.",
"note": "The headline holds - a uuid identifies no single line - but both the numbers and the attributed mechanism needed correcting, and one half of the cross-file story is refuted outright. Only 2.6 percent of repeat pairs straddle a compaction boundary, so the compaction re-anchor is a minor contributor; the dominant shape is a resume that re-appends the entire prior history into the same file behind a session-state line triple, with the second copy identifiable by a newly added slug key. The clone half of the claim is confirmed by direct measurement on the corpus's single clone (558 uuids present in both files). The spawn-copy half is false here: across 1876 parent-child transcript pairs not one uuid is shared, so a subagent transcript mints fresh uuids rather than inheriting the spawning message's."
}
]
},
{
"id": "REC-092",
"area": "record-model",
"behavior": "Within ONE transcript a duplicated `uuid` has more than one physical line, and the copies are not interchangeable: measured over the 5 transcripts (of 39 sized 200KB-40MB) that carry intra-file repeats, 14 duplicated uuids are assistant records on both copies and 3 of those 14 carry a LATER copy whose four flat `message.usage` fields are all zero while the earlier copy carries the real numbers. Claude Code ships a zero-filled usage template that matches that shape exactly.",
"depends": "The two `show` paths resolve a duplicated uuid DIFFERENTLY, and the difference is load-bearing. The RENDERED path (the default) treats `--uuid` as a membership test, so it emits EVERY copy - measured 2 record rows for a uuid the census says appears twice. The `--raw` path binds each requested uuid to the FIRST line whose OWN `uuid` field equals it (a body merely quoting the uuid does not satisfy the address) and therefore emits the earliest copy only - measured 1 line, the one carrying the real usage numbers.",
"code": [
{
"path": "src/show/run.rs",
"lines": "129-137",
"snippet": " for ((u, slot), finder) in uuid_line.iter_mut().zip(&uuid_finders) {\n if slot.is_none() && finder.find(line).is_some() {\n // Confirm structurally: the record's OWN uuid must equal it (a body\n // merely quoting the uuid must not satisfy the address).\n if let Ok(Some(rec)) = crate::parse::parse_line(line) {\n if rec.uuid.as_deref() == Some(u.as_str()) {\n *slot = Some(line_no);\n want = true;\n }"
},
{
"path": "src/search/matcher.rs",
"lines": "543-549",
"snippet": "/// Record-address selectors (`--line` / `--uuid`) parsed into membership sets - the \"fetch\n/// THESE records\" filter that turns `search` into the in-permission message-getter. Active when\n/// either set is non-empty; a record is addressed when its physical line OR uuid is in range.\npub(crate) struct AddressSet {\n pub(crate) lines: BTreeSet<usize>,\n pub(crate) uuids: BTreeSet<String>,\n}"
}
],
"instrument": "Find a duplicated uuid with the Python census, then run BOTH `csift show @<id> --uuid <uuid> --format json` (expect one row per copy) and `csift show @<id> --uuid <uuid> --raw` (expect exactly one line, the earliest copy). Counting rule: rows with kind==\"record\" in the rendered jsonl stream; non-blank stdout lines for `--raw`.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/show/run.rs:125-141"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 dupscan.py (per file in ~/.claude/projects/*/*.jsonl sized 200KB-40MB: json-parse every line carrying \"uuid\", Counter the top-level `uuid`, keep values with count>1); then for one duplicated assistant uuid: `csift show @<id> --uuid <uuid> --format json` and `csift show @<id> --uuid <uuid> --raw`",
"observed": "5 of 39 band files carry intra-file duplicate top-level uuids; the repeats are contiguous replay blocks (first file: lines 25->4010, 26->4011, 35->4012). Across those 5 files 14 duplicated uuids are assistant-on-both-copies; 3 of the 14 carry DIFFERING usage and in all 3 the later copy is all-zero, e.g. lines 2508/2601 -> (input,output,cache_read,cache_creation) = (2,4849,951431,16024) vs (0,0,0,0). `csift show --uuid <that uuid> --format json` emitted RECORD_ROWS=2 (line 2508 and line 2601, both label agent.tool.use); the same address with `--raw` emitted 1 line, the line-2508 copy carrying the real numbers. The zero shape is a literal in the harness binary: `var mm={output_tokens_details:{thinking_tokens:0},input_tokens:0,cache_creation_input_tokens:0,cache_read_input_tokens:0,output_tokens:0,server_tool_use:{web_search_requests:0,web_fetch_requests:0},service_tier:\"standard\",cache_creation:{ephemeral_1h_input_tokens:0,ephemeral_5m_input_tokens:0},inference_geo:\"\",iterations:[],speed:\"standard\"}`",
"rule": "One observation per (file, top-level uuid value) with count>1; usage compared as the 4-tuple of the flat token fields with a missing field read as 0. Row count = jsonl lines with kind==\"record\" (header and summary excluded) for the rendered path, non-blank stdout lines for `--raw`.",
"note": "The claimed observable half is confirmed by instrument. The `depends` half was wrong for the command's DEFAULT path: the rendered path emitted 2 rows, not 1. First-wins is the `--raw` path's rule only. Both code snippets exist verbatim in the current tree at the lines given."
}
]
},
{
"id": "REC-093",
"area": "record-model",
"behavior": "An explicit `show` address that resolves to no record is a HARD failure, not an empty result: csift bails with `no such record(s): ` naming each miss (a single `--line` token with no `..` is an explicit address, a `..` token is a clamping range that must still yield at least one record).",
"depends": "This is the durability guarantee's enforcement half - a stale refetch printed by an earlier search cannot come back as a silent empty; if Claude Code ever renumbered lines, every stale address would surface as a loud miss rather than as wrong content.",
"code": [
{
"path": "src/show/run.rs",
"lines": "164-166",
"snippet": " if !misses.is_empty() {\n bail!(\"no such record(s): {}\", misses.join(\", \"));\n }"
},
{
"path": "src/show/run.rs",
"lines": "251-253",
"snippet": " if !misses.is_empty() {\n bail!(\n \"no such record(s): {} — an explicit address renders message lines \\"
}
],
"instrument": "`csift show @<id> --line 999999 --raw; echo $?` -> stderr `csift: error: no such record(s): L999999 (file has N lines)` and exit 1. The DEFAULT (rendered) path bails from a different site and prints a different tail: `csift: error: no such record(s): L999999 - an explicit address renders message lines ...` with no `(file has N lines)` suffix. Counting rule: exit code non-zero and the literal `no such record(s): ` present; do not key on the suffix, it is path-dependent.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/show/run.rs:164-166"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift show @<id> --line 999999 --raw; echo $? / csift show @<id> --line 999999; echo $? / csift show @<id> --uuid 00000000-0000-0000-0000-000000000000; echo $? / csift show @<id> --line 999999..1000005 --raw; echo $? / csift show @<id> --line 159034..999999 --raw; echo $?",
"observed": "raw single: `csift: error: no such record(s): L999999 (file has 159036 lines)` exit 1. raw range wholly beyond EOF: `csift: error: no such record(s): L999999-1000005 (file has 159036 lines)` exit 1. rendered single: `csift: error: no such record(s): L999999 - an explicit address renders message lines (role:user/role:assistant, superseded drafts included), attachment lines, and the promoted non-record lines (a queue-operation with text, turn_duration, away_summary, stop_hook_summary, file-history-snapshot/-delta); session-state cache lines (last-prompt, mode, ai-title, ...), a content-less queue dequeue, unpromoted system subtypes and torn lines are inspectable with `--raw`' exit 1. rendered uuid miss: same message with `uuid 00000000-...`, exit 1. Partially-beyond range clamped: 4 lines on stdout, exit 0.",
"rule": "One observation per invocation; verdict = (exit code non-zero) AND (stderr contains the literal `no such record(s): `). A clamping range counts as a pass when it exits 0 with at least one stdout line.",
"note": "Behaviour holds exactly - an unresolvable explicit address is a hard non-zero failure on both paths, never a silent empty, and a partially-beyond range still clamps and succeeds. Only the expected stderr literal in the instrument needed correcting: it describes the `--raw` site, while the default path bails from the second site with a different tail. Both bail sites and the `LineSpecs` snippet exist verbatim in the current tree."
}
]
},
{
"id": "REC-094",
"area": "record-model",
"behavior": "A line number is per-FILE, and a subagent transcript is a different file from its parent session, so an address is only meaningful paired with the transcript that owns it.",
"depends": "csift's printed refetch is always addressed at the hit's OWN `session_id`, never the parent uuid - in text mode a subagent hit prints an extra `csift show @<agent-id> --line N` continuation - because pointing the parent at a subagent's line would silently fetch the WRONG record.",
"code": [
{
"path": "src/search/render.rs",
"lines": "398-400",
"snippet": "/// The ready-to-run fetch command for a hit - `csift show` addressed at the transcript that\n/// OWNS the hit's line number (`session_id`, never the parent uuid: line numbers are\n/// per-file, so pointing the parent at a subagent's line would silently fetch the WRONG"
}
],
"instrument": "`csift search '<pattern>' <project> --format json | jq -r 'select(.kind==\"hit\") | .refetch'` on a scope containing subagent hits: every emitted command must name the hit's own `session_id`, and running one must return the same record the hit reported. Counting rule: one refetch per hit object.",
"located": {
"claude_code": null,
"csift": "0.10.0",
"source": "src/search/render.rs:398-410"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search 'clippy' . --max-count 400 --format json (run from the csift repo root, so the project target is the repo's own transcripts), piped through a Python counter over every object carrying `hits`; then for 3 subagent hits, run the emitted `refetch` verbatim and compare, and run the SAME line number addressed at `parent_session_id`; plus `csift search 'clippy' . --max-count 60 | grep -c '↳ csift show @'`",
"observed": "hits_with_refetch=1284, names_own_session_id=1284, mismatch=0, subagent_hits=431. Every subagent sample had parent_session_id != session_id. Round-trip: 3/3 own-refetch invocations returned exactly 1 record row whose `uuid` and `line` equalled the hit's. Counter-probe: 3/3 of the SAME line numbers addressed at the parent session returned a record whose uuid did NOT equal the hit's - the silent-wrong-record hazard, reproduced. Text mode printed 259 `↳ csift show @<agent-id> --line N` continuations in a 60-hit window.",
"rule": "One observation per hit object carrying a non-null `refetch`; a refetch 'names its own id' iff it starts with the literal `csift show @` + that hit's `session_id` + a space. Round-trip pass iff exactly one kind==\"record\" row comes back with the hit's uuid and line.",
"note": "Confirmed in both directions: the printed address is always the owning transcript's id (0/1284 exceptions), it round-trips to the identical record, and the parent-addressed variant of the same line number fetches a different record on every probe. The doc-comment snippet exists verbatim at src/search/render.rs:398-400."
}
]
},
{
"id": "REC-095",
"area": "record-model",
"behavior": "Claude Code's transcript writer emits non-ASCII text VERBATIM as UTF-8 and never as a `\\uXXXX` escape: over the whole live corpus of 67 top-level transcripts, inspecting the first 4,000 bytes of each of 804,007 non-blank lines, 178,918 lines (22.25%) carry a byte >= 0x80 and 0 lines carry a writer-emitted `\\uXXXX` escape for a codepoint >= U+0080 (130 regex hits were all a literal two-character backslash-u sequence inside quoted content, not an escape).",
"depends": "This is exactly why csift's needle-safety predicate keeps non-ASCII needles prefilter-eligible, and why the product can promise regex over CJK at all; if the writer switched to ASCII-safe output, every multi-byte pattern would stop matching at the byte prefilter, before any counter could see it - the silent-drop failure `required_needles` exists to prevent.",
"code": [
{
"path": "src/search/matcher.rs",
"lines": "538-540",
"snippet": "pub(crate) fn json_escapes_in_string(c: char) -> bool {\n c == '\"' || (c as u32) < 0x20 || c == '\\u{7f}'\n}"
}
],
"instrument": "Python over a size-filtered random sample: per line take `raw[:4000]`, count the line if any byte >= 0x80, and separately count it if `re.search(rb'\\\\u00[89a-fA-F][0-9a-fA-F]|\\\\u0[1-9a-fA-F][0-9a-fA-F]{2}|\\\\u[1-9a-fA-F][0-9a-fA-F]{3}', head)` matches; for each escape hit check whether the raw bytes contain `\\\\\\\\u` (a literal backslash-u in content) and exclude it. Counting rule: one observation per line, prefix-bounded at 4,000 bytes.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 sweep95_99.py (walks EVERY ~/.claude/projects/*/*.jsonl; per non-blank line take raw[:4000], count it if any byte >= 0x80, and separately count it if re.search(rb'\\\\u00[89a-fA-F][0-9a-fA-F]|\\\\u0[1-9a-fA-F][0-9a-fA-F]{2}|\\\\u[1-9a-fA-F][0-9a-fA-F]{3}') matches; a hit is excluded when the raw bytes contain a doubled backslash before the u)",
"observed": "files_scanned=67, lines_total=804007, lines_nonascii=178918 (22.25%), esc_regex_hits=130, esc_hits_excluding_literal_backslash_u=0. Sampled excluded hits are all literal two-character backslash-u inside quoted content, e.g. `FIRST verify both respond \\\\u2014 ` and `<name>.md \\\\u2014 scratch markdown fixtur`.",
"rule": "One observation per non-blank line, prefix-bounded at 4,000 bytes; denominator = all non-blank lines of all top-level transcripts.",
"note": "Substance confirmed at 12x the claimed sample: still zero writer-emitted escapes, and the non-ASCII share is within 0.15 points of the claimed figure. Numbers restated on a whole-corpus rule a stranger can rerun without knowing which files were sampled. The `json_escapes_in_string` snippet exists verbatim at src/search/matcher.rs:537-539."
}
]
},
{
"id": "REC-096",
"area": "record-model",
"behavior": "An assistant record's `message.usage` object carries eleven keys - the same eleven the harness binary's zero-filled usage template enumerates - measured over the whole live corpus with one observation per assistant record carrying a dict `usage` (133,935 records): `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, `service_tier`, `cache_creation`, `inference_geo` present on all 133,935; `iterations` on 133,800 (99.90%); `server_tool_use` and `speed` on 133,798 (99.90%); `output_tokens_details` on 22,044 (16.46%, of which 23 carry JSON null rather than an object).",
"depends": "csift's `TokenUsage` probe deserializes exactly four of the eleven (`input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`), so `csift stats` reports no thinking-token split, no server-tool counts, no service tier and no inference geography; adding any of them means reading a key whose absence rate is already measured.",
"code": [
{
"path": "src/model/record.rs",
"lines": "270-275",
"snippet": "pub struct TokenUsage {\n pub input_tokens: Option<u64>,\n pub output_tokens: Option<u64>,\n pub cache_read_input_tokens: Option<u64>,\n pub cache_creation_input_tokens: Option<u64>,\n}"
}
],
"instrument": "Python over a size-filtered random sample: for every line with `type == \"assistant\"` whose `message.usage` is a dict, increment a `Counter` over its keys; report each key's count against the record total. Counting rule: one observation per assistant RECORD (not per API message), so the per-block duplication is included by design.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 sweep95_99.py (whole corpus; for every line with type==\"assistant\" whose message.usage is a dict, Counter over its keys) AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var mm=\\{output_tokens_details.{0,330}'",
"observed": "usage_records=133935. Key counts: input_tokens / output_tokens / cache_read_input_tokens / cache_creation_input_tokens / service_tier / cache_creation / inference_geo = 133935 each (100%); iterations = 133800 (99.90%); server_tool_use = 133798 (99.90%); speed = 133798 (99.90%); output_tokens_details = 22044 (16.46%). Exactly 11 distinct keys, no twelfth. The harness binary carries a zero-filled template naming those same eleven: `var mm={output_tokens_details:{thinking_tokens:0},input_tokens:0,cache_creation_input_tokens:0,cache_read_input_tokens:0,output_tokens:0,server_tool_use:{web_search_requests:0,web_fetch_requests:0},service_tier:\"standard\",cache_creation:{ephemeral_1h_input_tokens:0,ephemeral_5m_input_tokens:0},inference_geo:\"\",iterations:[],speed:\"standard\"}`",
"rule": "One observation per assistant RECORD whose message.usage is a dict (per-block duplication of one API message's usage is included by design); a key counts once per record it appears on, regardless of value.",
"note": "The eleven-key set is confirmed twice over - by census and by a literal template in the 2.1.258 binary. Two numbers needed correcting: `iterations` is present on 2 more records than `server_tool_use`/`speed` (the claim grouped all three at one figure), and `output_tokens_details` runs at 16.46% here rather than 6.4%, so its absence rate is sample-sensitive and should not be treated as a constant. The TokenUsage snippet exists verbatim at src/model/record.rs:269-274 and still reads only four of the eleven."
}
]
},
{
"id": "REC-097",
"area": "record-model",
"behavior": "Three of the `message.usage` values are nested objects with fixed shapes: `cache_creation` carries `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens` (both present on all 133,935 records that carry a dict `cache_creation`), `server_tool_use` carries `web_search_requests` and `web_fetch_requests` (both on all 133,798), and `output_tokens_details` carries `thinking_tokens` (on all 22,021 records where it is an object). `output_tokens_details` is the one that is not always an object: of the 22,044 records carrying the key, 23 carry JSON `null`.",
"depends": "csift reads none of the nested objects, so `stats` cannot report the 5m/1h cache split, web-tool call counts, or a thinking/non-thinking output split; a future addition must know these are nested, not flat.",
"code": [
{
"path": "src/model/record.rs",
"lines": "281-284",
"snippet": " pub fn token_usage(&self) -> Option<TokenUsage> {\n let raw = self.usage.as_ref()?;\n serde_json::from_str(raw.get()).ok()\n }"
}
],
"instrument": "Same Python sweep, additionally iterating the keys of `usage['cache_creation']`, `usage['server_tool_use']` and `usage['output_tokens_details']` when each is a dict. Counting rule: one observation per (record, nested key) pair.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 sweep95_99.py (whole corpus; iterate the keys of usage['cache_creation'], usage['server_tool_use'] and usage['output_tokens_details'] when each is a dict) AND python3 -c ... (classify every present output_tokens_details value as null / dict / other) AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var mm=\\{output_tokens_details.{0,330}'",
"observed": "cache_creation dict on 133935 records, subkeys ephemeral_1h_input_tokens=133935 and ephemeral_5m_input_tokens=133935 (both on every one). server_tool_use dict on 133798, subkeys web_search_requests=133798 and web_fetch_requests=133798 (both on every one). output_tokens_details present on 22044: dict on 22021 (subkey thinking_tokens on all 22021), JSON null on 23, other 0. The binary template shows the same three nested shapes.",
"rule": "One observation per (record, nested key) pair; a nested value that is not a dict is counted separately rather than silently skipped.",
"note": "Nested shapes confirmed by census and by the binary's zero-filled template. One addition the claim missed: `output_tokens_details` can be JSON null (23 of 22,044 records), so a future reader must handle null before indexing `thinking_tokens`. The `token_usage` snippet exists verbatim at src/model/record.rs:280-283."
}
]
},
{
"id": "REC-098",
"area": "record-model",
"behavior": "`cache_creation.ephemeral_5m_input_tokens + cache_creation.ephemeral_1h_input_tokens` equals the flat `cache_creation_input_tokens` on 133,798 of 133,935 whole-corpus records (99.898%) and disagrees on 137 (0.102%) - so the nested pair very nearly PARTITIONS the flat field but is not guaranteed to. Every disagreement is one-hour-cache-specific: all 137 have a zero 5m bucket and a non-zero 1h bucket, 101 of them with a flat field of 0 (the whole 1h amount missing from the flat field) and 36 with a non-zero flat field that still differs from the 1h amount.",
"depends": "Summing the nested pair alongside the flat field would double-count cache-creation tokens in `csift stats` on 99.9% of records; a future implementation must pick one or the other. Claude Code's own resolution is worth copying rather than inventing: it takes the flat field when it is greater than zero and falls back to the nested sum otherwise, which is exactly what rescues the 101 flat-zero records.",
"code": [
{
"path": "src/stats.rs",
"lines": "258-263",
"snippet": " let vals = [\n u.input_tokens.unwrap_or(0),\n u.output_tokens.unwrap_or(0),\n u.cache_read_input_tokens.unwrap_or(0),\n u.cache_creation_input_tokens.unwrap_or(0),\n ];"
}
],
"instrument": "Python over a size-filtered random sample: for each assistant record with a dict `usage.cache_creation`, compare `(ephemeral_5m_input_tokens or 0) + (ephemeral_1h_input_tokens or 0)` against `(cache_creation_input_tokens or 0)`; report equal and unequal counts. Counting rule: one observation per record; missing sub-keys treated as 0.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 sweep95_99.py and python3 ccdetail.py (whole corpus; for each assistant record with a dict usage.cache_creation compare (ephemeral_5m_input_tokens or 0)+(ephemeral_1h_input_tokens or 0) against (cache_creation_input_tokens or 0)) AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,90}cache_creation\\?\\.ephemeral_5m_input_tokens.{0,90}'",
"observed": "partition_equal=133798, partition_unequal=137 of 133935 records (99.898% / 0.102%). Every one of the 137 disagreements has ephemeral_5m_input_tokens=0 and ephemeral_1h_input_tokens>0: in 101 the flat field is 0 while the nested sum is not (e.g. flat 0 vs sum 913; flat 0 vs sum 16249), and in 36 the flat field is non-zero but unequal (e.g. flat 565371 vs e1h 618002; flat 529421 vs e1h 803948). 132,587 records carry a non-zero ephemeral_1h_input_tokens and 132,450 of those agree. The harness binary resolves the conflict with an explicit preference: `_=r?.cache_creation_input_tokens??0,v=(r?.cache_creation?.ephemeral_5m_input_tokens??0)+(r?.cache_creation?.ephemeral_1h_input_tokens??0),C=_>0?_:v`",
"rule": "One observation per assistant record with a dict cache_creation; missing sub-keys read as 0; equality is exact integer equality.",
"note": "Substance holds; the agreement rate is 0.8 points higher than claimed on the whole corpus, and the disagreement has a shape the claim did not name - it is entirely a one-hour-cache accounting gap, and the harness itself carries the flat-else-sum fallback rule. The stats.rs four-value snippet exists verbatim at src/stats.rs:258-263."
}
]
},
{
"id": "REC-099",
"area": "record-model",
"behavior": "`output_tokens_details.thinking_tokens` is a SUBSET of `output_tokens` on every record that reports a non-zero `output_tokens`: over the whole corpus, 22,019 of 22,021 records carrying the key satisfy `thinking_tokens <= output_tokens` (99.991%). The 2 exceptions are not an accounting inversion but a zeroed-usage artifact - two per-block records of ONE API message whose four flat token fields are all 0 while the nested `thinking_tokens` (2218) survived the zeroing.",
"depends": "If `csift stats` ever reports a thinking split it must SUBTRACT rather than add, otherwise the per-model output total would exceed what the API billed. The subtraction must also be floored: a record whose flat usage was zeroed while the nested thinking count survived would otherwise produce a negative non-thinking output figure.",
"code": [
{
"path": "src/stats.rs",
"lines": "307",
"snippet": " for ((model, _), vals) in usage_peak {"
}
],
"instrument": "Python over a size-filtered random sample: for each assistant record with a dict `usage.output_tokens_details` carrying `thinking_tokens`, compare it against `usage.output_tokens`; report the `<=` and `>` counts. Counting rule: one observation per record carrying the key.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 sweep95_99.py and python3 thinkviol.py (whole corpus; for each assistant record with a dict usage.output_tokens_details carrying thinking_tokens, compare it against usage.output_tokens, then re-scan for the violating records and read their message.id, block types and flat usage fields)",
"observed": "thinking_le_output=22019, thinking_gt_output=2 over 22,021 records carrying the key (99.991% / 0.009%). Both violations are in one transcript at adjacent lines 30 and 31, share one `message.id`, carry one content block each (thinking, then text), and have thinking_tokens=2218 with input_tokens=output_tokens=cache_read_input_tokens=cache_creation_input_tokens=0 - a fully zeroed flat usage whose nested thinking count survived. Restricting to records with output_tokens > 0 gives 22,019 observations and 0 violations.",
"rule": "One observation per assistant record whose usage.output_tokens_details is a dict carrying thinking_tokens; missing output_tokens read as 0; violation iff thinking_tokens > output_tokens.",
"note": "This is the one claim whose absolute was refuted. The claim asserted 0 violations over 456 sampled records; the whole corpus has 2 violations over 22,021. Both are explained - and the explanation strengthens the claim's own dependent, since the zeroed-usage shape is the same one that makes the per-field-MAX dedupe necessary - but the invariant must now be stated conditionally, not absolutely. The stats.rs line 307 snippet exists verbatim."
}
]
},
{
"id": "REC-100",
"area": "record-model",
"behavior": "A TOP-LEVEL transcript's filename basename equals the record-level `sessionId` on every observed file: over the whole live corpus, all 67 top-level transcripts carried a top-level `sessionId` on at least one record, 0 files carried a `sessionId` that differed from the basename, and 0 files carried more than one distinct `sessionId` value. The law does NOT extend to subagent transcripts: in a 150-file random sample of the 7,584 subagent transcripts, 150 of 150 carry the OWNING session's uuid as their `sessionId`, never their own basename.",
"depends": "csift derives every row's id from the FILENAME and keeps the data-derived `sessionId` only as a fallback when the filename yields nothing - which is what makes subagent ids correct at all, since a subagent transcript's own records name the parent. The registry join in `status` matches a row by `sessionId`, so filename and data agreeing is what makes that join work on a top-level session; on a subagent transcript the data id would resolve to the parent instead.",
"code": [
{
"path": "src/session/summarize.rs",
"lines": "82-87",
"snippet": " // Prefer the filename-derived id; cross-check with the data id (§2.4 spirit).\n let session_id = if session_id.is_empty() {\n data_session_id.unwrap_or_default()\n } else {\n session_id\n };"
},
{
"path": "src/live/registry.rs",
"lines": "57-59",
"snippet": " if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) {\n continue;\n }"
}
],
"instrument": "Python over every `~/.claude/projects/*/*.jsonl`: collect the set of top-level `sessionId` string values per file, compare against the basename minus `.jsonl`, and report files whose basename is absent from the set plus files with more than one distinct value. Counting rule: one observation per file that carries at least one `sessionId`.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 sweep95_99.py (over every ~/.claude/projects/*/*.jsonl: collect the set of top-level `sessionId` string values per file, compare against the basename minus .jsonl, report files whose basename is absent from the set and files with more than one distinct value) AND a second pass over subagent transcripts: 150 files randomly sampled (seed 11) from the 7,584 under ~/.claude/projects/*/*/subagents/**, excluding journal.jsonl, classifying each file's sessionId set as matching its own basename, matching the owning session uuid, or neither",
"observed": "Top level: files_scanned=67, files_with_sessionId=67, basename_mismatch_count=0, multi_sessionid_count=0. Subagents: sample n=150 -> basename match 0, owning-session uuid 150, other 0, no sessionId 0, more than one distinct value 0. A 30-file probe at a different seed gave the same 30/30 split beforehand.",
"rule": "One observation per file that carries at least one top-level `sessionId`; a file passes iff its basename minus `.jsonl` is a member of that file's sessionId set and the set has exactly one element.",
"note": "Holds for top-level transcripts with a one-larger denominator (67 files, not 66 - consistent with one session created since the claim was written). Added the scope caveat the claim was missing: the identity is top-level-only, and on subagent transcripts the record-level sessionId is the owning session's uuid. Both code snippets exist verbatim, at src/session/summarize.rs:82-87 and src/live/registry.rs:57-59."
}
]
},
{
"id": "SUB-001",
"area": "subagents",
"behavior": "Claude Code writes subagent transcripts in exactly three on-disk shapes under a session's sidecar directory `<ENCODED>/<session-uuid>/`: a built-in-location transcript at `subagents/agent-<id>.jsonl` (which hosts BOTH plain Task/Agent subagents and teammates - 833 files), a workflow subagent at `subagents/workflows/wf_<id>/agent-<hex>.jsonl` (6664 files), and one `subagents/workflows/wf_<id>/journal.jsonl` per workflow run (160, exactly one per wf_* dir). The journal is an EVENT log, not a transcript: every line carries `{agentId, key, type}`, and a `result` line carries a fourth key `result` holding the return payload; no journal line has a `message` or a `role`.",
"depends": "csift's `discover_subagents` enumerates only the two transcript shapes and hard-excludes `journal.jsonl` and every `.meta.json`, reading the journal only for completion status and the returned-message payload; listing the journal as a transcript would inject event objects into every spanning surface (`list`/`search`/`stats`/`files`/`recover`/`plan`/`image`/`status`).",
"code": [
{
"path": "src/subagent/discover.rs",
"lines": "18-21",
"snippet": "/// Walks `<session-uuid>/subagents/` for built-in `agent-<hex>.jsonl` and\n/// `<session-uuid>/subagents/workflows/wf_*/agent-<hex>.jsonl` for workflow agents.\n/// **`journal.jsonl` is excluded** (it is an event log, not a transcript), as is any\n/// non-`agent-*.jsonl` or `.meta.json` file. Returns an empty vec when the session"
},
{
"path": "src/subagent/meta.rs",
"lines": "110-113",
"snippet": "/// True iff the workflow journal alongside a workflow subagent carries a `result`\n/// event for `agent_id` (the completion signal, §C). For a built-in subagent (no\n/// journal) this is always `false` - completion is inferred from the transcript.\npub(crate) fn journal_reports_completion(subagent: &Subagent, journals: &JournalCache) -> bool {"
}
],
"instrument": "`find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' | sed 's|.*/subagents/||;s|/[^/]*$||' | sort | uniq -c` - expect two prefixes only: an empty one (built-in, flat) and `workflows/wf_<id>`; `find ~/.claude/projects -name journal.jsonl | wc -l` must equal the number of `wf_*` dirs; then `csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | .shape' | sort | uniq -c` and confirm no row is named `journal`. Counting rule: one row per file, journals excluded.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "AGENTS.md section 3.1; AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5; src/subagent/discover.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' | sed 's|.*/subagents/||' | python3 -c \"import sys,re,collections; c=collections.Counter(); [c.update(['<flat>/agent-<hex>.jsonl' if '/' not in l.strip() else re.sub(r'wf_[^/]*','wf_<id>','/'.join(l.strip().split('/')[:-1]))+'/'+re.sub(r'^agent-.*\\\\.jsonl','agent-<hex>.jsonl',l.strip().split('/')[-1])]) for l in sys.stdin]; [print(v,k) for k,v in c.most_common()]\" ; find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 cat | jq -c 'keys' | sort | uniq -c ; find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 cat | jq -r 'select(has(\"message\") or has(\"role\"))' | wc -l ; for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --format json | jq -r 'select(.kind==\"agent\")|.shape'; done | sort | uniq -c ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,50}journal\\.jsonl.{0,50}'",
"observed": "Exactly three path shapes under subagents/: 6664 'workflows/wf_<id>/agent-<hex>.jsonl', 833 flat 'agent-<hex>.jsonl', 160 'workflows/wf_<id>/journal.jsonl'. journal count 160 == wf_* dir count 160. Journal line key sets, whole corpus: [\"agentId\",\"key\",\"type\"] x6665 and [\"agentId\",\"key\",\"result\",\"type\"] x6043 (12708 lines total); 0 lines carry a 'message' or 'role' key. csift agents shape values corpus-wide: builtin-task 669, teammate 164, workflow 6845 - no row named 'journal'. Binary 2.1.258 carries 'this.path=On(hH(n),\"journal.jsonl\")' and '\"subagents\",...n.agentRelPath,\"journal.jsonl\")' and 'Per-agent results: ${d}/journal.jsonl \\u2014 one {\"type\":\"result\",...} line per complet'.",
"rule": "One row per file for the path-shape census (journals counted separately); one row per journal LINE for the key-set census; one row per csift kind==\"agent\" JSON row for the shape census, over every session directory that has a subagents/ dir.",
"note": "Structure and exclusion both hold. Refined on two counts: the claim's journal key set is incomplete (a result event carries a fourth `result` key, 6043 of 12708 lines), and the flat built-in location is shared by two csift shapes (builtin-task and teammate), so 'built-in Task/Agent subagent' under-describes what lands there."
}
]
},
{
"id": "SUB-002",
"area": "subagents",
"behavior": "A subagent transcript's canonical agent id is the filename stem minus the on-disk `agent-` prefix, and it is byte-identical to the `agentId` Claude Code writes in the transcript records and in the workflow journal (7497/7497, 0 mismatches). That id is a BARE hex for 7333 of 7501 stems; the remaining 168 are the name-embedding teammate form `a<Name>-<16 hex>`. The owning session uuid is the directory component immediately before the `subagents` segment, and neither id form is itself a re-feedable top-level session uuid.",
"depends": "csift derives every emitted row's id trio (`session_id`, `is_subagent`, `parent_session_id`) through the `subagent` helpers rather than by reading the path stem: the stem yields an `agent-`-prefixed id that joins to nothing and resolves through no `@` target, and only the parent uuid is re-feedable for a downstream fetch.",
"code": [
{
"path": "src/subagent/ids.rs",
"lines": "5-12",
"snippet": "/// Strip the on-disk `agent-` filename prefix to the bare-hex canonical agent id (the\n/// value the transcript record's `agentId` field AND the workflow journal carry). The\n/// single source of truth for this rule - used by `make_subagent` and by the\n/// `recover` / `session` / `files` subcommands so a subagent row's printed `session_id`\n/// is the SAME bare hex `agents` prints, hence joinable across surfaces.\n#[must_use]\npub fn bare_agent_id(stem: &str) -> &str {\n stem.strip_prefix(\"agent-\").unwrap_or(stem)"
},
{
"path": "src/subagent/ids.rs",
"lines": "32-36",
"snippet": "/// The re-feedable PARENT session uuid for a transcript path, or `None` when the path is a\n/// top-level `<uuid>.jsonl` (which IS its own session). A subagent transcript lives at\n/// `…/<PARENT-UUID>/subagents/[workflows/wf_*/]agent-<hex>.jsonl`, so the parent uuid is the\n/// directory component immediately BEFORE the `subagents` segment. This is what makes a\n/// search/files subagent match re-feedable: its bare-hex `session_id` is NOT a re-feedable"
},
{
"path": "src/subagent/ids.rs",
"lines": "10-45",
"snippet": "#[must_use]\npub fn parent_session_id_from_path(path: &Path) -> Option<String> {\n let mut prev: Option<&str> = None;\n for comp in path.components() {\n let c = comp.as_os_str().to_str()?;\n if c == \"subagents\" {\n // The component just before `subagents` is the parent-session dir name.\n return prev.map(str::to_string);"
}
],
"instrument": "`csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | .agent_id' | head -1`, then `rg -m1 -o '\"agentId\":\"[^\"]*\"' <that agent's jsonl>` - the two must be string-equal and must equal the filename stem minus the leading `agent-`; feeding the printed id back as `csift show @<id> --turn -1` must fetch rather than error. Counting rule: one equality test per subagent file; every id csift prints must round-trip through the @-grammar.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "AGENTS.md section 3.7; SPEC.md section 1; src/subagent/meta.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import glob,os,re,pathlib; files=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/agent-*.jsonl'),recursive=True); eq=neq=noid=0\\nfor p in files:\\n bare=os.path.basename(p)[6:-6]; found=None\\n for line in open(p,errors='replace'):\\n m=re.search(r'\\\"agentId\\\":\\\"([^\\\"]*)\\\"',line)\\n if m: found=m.group(1); break\\n eq+= found==bare; neq+= found is not None and found!=bare; noid+= found is None\\nprint(len(files),eq,neq,noid)\" ; ok=0; fail=0; for d in $(find ~/.claude/projects -type d -name subagents | head -8); do u=$(basename \"$(dirname \"$d\")\"); for a in $(csift agents \"@$u\" --format json | jq -r 'select(.kind==\"agent\")|.agent_id' | head -8); do csift show \"@$a\" --turn -1 >/dev/null 2>&1 && ok=$((ok+1)) || fail=$((fail+1)); done; done; echo \"$ok $fail\" ; f=$(find ~/.claude/projects -path '*/subagents/*' -name 'agent-*.jsonl' | head -1); csift show \"$f\" --line 1 --format json | jq -c 'select(.kind==\"header\")|{is_subagent}'",
"observed": "7497 of 7497 subagent transcripts: filename stem minus the `agent-` prefix == the FIRST in-record `agentId`; 0 mismatches, 0 files with no agentId. 160 of 160 workflow journals: the wf_* dir's agent file stems are a subset of that journal's agentId values. 48 of 48 agent ids printed by `csift agents` fetched successfully through `csift show @<id> --turn -1` (exit 0). csift's printed parent_session_id equals the directory component immediately before `subagents`. Stem-shape census over 7501 stems: 7333 match `^[0-9a-f]+$` (all length 17), 168 match `^a[A-Za-z0-9-]+-[0-9a-f]{16}$`.",
"rule": "One equality test per subagent transcript file (stem minus `agent-` vs the first `\"agentId\":\"...\"` byte match in that file); one fetch attempt per id printed by `csift agents`, counted as OK on exit 0.",
"note": "The equality and the parent-uuid derivation both hold exactly. Refined because 'the canonical agent id is the BARE <hex>' is false for 168 of 7501 stems (the teammate form); the correct invariant is 'the stem minus the agent- prefix', which holds for all of them and round-trips as an @-target in 48 of 48 attempts."
}
]
},
{
"id": "SUB-003",
"area": "subagents",
"behavior": "Subagent transcripts are stored FLAT under one session directory at every depth: a subagent spawned by another subagent lands in the SAME `subagents/` dir, so nesting is never encoded in the path and exists only as the child's `spawn_tool_use_id` joined to the issuing `Task`/`Agent`/`Workflow` tool_use, which is recorded in the SPAWNING agent's transcript rather than the main one (117 subagent transcripts carry such a tool_use, all at the flat built-in location, 0 under `workflows/`). On-disk nesting is still zero (0 `subagents/*/subagents/*` files across 7501 subagent transcripts), but LOGICAL depth is no longer uniform: 138 nodes resolve to depth 1, 32 to depth 2 and 20 to depth 3, all with a parent distinct from themselves.",
"depends": "csift rebuilds the tree logically: parent_agent_id comes from the meta's own parentAgentId when the harness wrote one, else from a GLOBAL spawn index over the main transcript plus every subagent transcript, in both cases rejecting a value that names the node itself (a /fork child's cloned transcript carries its own spawning tool_use, so the graph join self-names); assign_depths then walks the id-to-parent chain (cycle-guarded) for depth. A path-derived tree would report every agent at depth 0, and the defensive recursion into nested subagents/ dirs is insurance for a future layout, not a present-data fix. The global spawn index folds the main transcript first and the subagent locals in discovery order with FIRST-wins on every id-keyed map (v0.10.2): a clone's copy of a sibling's spawn record never displaces the original issuer.",
"code": [
{
"path": "src/subagent/topology.rs",
"lines": "7",
"snippet": "/// returned message) and the per-node files-changed list. `children` is the tool_use-graph"
},
{
"path": "src/subagent/discover.rs",
"lines": "69-72",
"snippet": " // (C) DEFENSIVE recursion (insurance, not a present-data fix). Verified 2026-06-07:\n // across all 2348 subagent transcripts on disk there are ZERO sub-sub-agents - the\n // real layout is single-level FLAT (a child of a general-purpose subagent would land\n // flat in this SAME `subagents/` dir, already covered by (A)). But if a FUTURE Claude"
},
{
"path": "src/subagent/topology.rs",
"lines": "5-9",
"snippet": "/// One fully-linked subagent node in the topology (§new-model). Carries the flat\n/// lifecycle facts PLUS the toolUseId-linked spawn linkage (trigger time, parent agent,\n/// returned message) and the per-node files-changed list. `children` is the tool_use-graph\n/// nesting (real multi-level chains exist since CC 2.1.25x: measured 190 nodes at depth\n/// 1-3 with a distinct parent across the corpus)."
},
{
"path": "src/subagent/topology.rs",
"lines": "346-355",
"snippet": " // Parent agent: the meta's own `parentAgentId` when the harness wrote one, else the\n // tool_use-graph join - and NEVER the node itself. A `/fork` child is a clone of its\n // parent's transcript, so the spawning tool_use sits in the child's own file and the\n // graph join names the child as its own parent (measured: every depth-65 node was\n // such a self-cycle before v0.10.1).\n let not_self = |p: String| (p != subagent.agent_id).then_some(p);\n let parent_agent_id = subagent\n .meta_parent_agent_id\n .clone()\n .and_then(not_self)"
},
{
"path": "src/subagent/meta.rs",
"lines": "73-77",
"snippet": " /// `parentAgentId` - the harness's own word on the spawning agent (written since CC\n /// 2.1.25x; 92 metas in the reference corpus). Load-bearing for a `/fork` child: its\n /// transcript is a CLONE of the parent's and therefore carries the spawning tool_use\n /// itself, so the tool_use-graph join names the child as its own parent. This field\n /// breaks that self-cycle (v0.10.1)."
},
{
"path": "src/subagent/spawn.rs",
"lines": "101-110",
"snippet": " pub(crate) fn merge(&mut self, other: ParentSpawnIndex) {\n for (k, v) in other.spawns {\n self.spawns.entry(k).or_insert(v);\n }\n for (k, v) in other.tool_results {\n self.tool_results.entry(k).or_insert(v);\n }\n for (k, v) in other.issuer {\n self.issuer.entry(k).or_insert(v);\n }"
}
],
"instrument": "`find ~/.claude/projects -path '*/subagents/*/subagents/*' -name '*.jsonl' | wc -l` must be 0; `csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | .depth' | sort | uniq -c` - any value above 1 would be the first observed nesting. Counting rule: one row per subagent transcript file; one depth per node.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "AGENTS.md section 3.7; SPEC.md section 6.5; SPEC.md section 10.3; src/subagent/discover.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "find ~/.claude/projects -path '*/subagents/*/subagents/*' -name '*.jsonl' | wc -l ; for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --format json | jq -r 'select(.kind==\"agent\")|.depth'; done | sort -n | uniq -c ; for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --format json | jq -r 'select(.kind==\"agent\" and .depth>0)|[.depth,(if .parent_agent_id==.agent_id then \"SELF\" else \"OTHER\" end)]|@tsv'; done | sort | uniq -c ; find ~/.claude/projects -path '*/subagents/*' -name 'agent-*.jsonl' -print0 | xargs -0 rg -l '\"name\":\"(Task|Agent|Workflow)\"' | wc -l",
"observed": "0 nested `subagents/*/subagents/*` transcripts (the flat layout holds). Corpus is now 7501 subagent transcripts, not 2348. csift depth census: 7456 at depth 0, 138 at depth 1, 32 at depth 2, 20 at depth 3, 32 at depth 65. Of the 222 depth>0 rows, 190 have a parent_agent_id DIFFERENT from their own agent_id (genuine multi-level chains) and 32 have parent_agent_id == agent_id (a self-parent, all with agent_type 'fork'). 117 subagent transcripts carry a `Task|Agent|Workflow` tool_use, every one of them at the flat built-in location and 0 under `workflows/`.",
"rule": "One row per csift kind==\"agent\" JSON row, over every session directory that has a subagents/ dir; one file per `rg -l` hit for the spawn-tool-use probe; nested-directory count is one row per file.",
"note": "Drifted on the measured half. Two separate findings. (1) Genuine multi-level nesting now exists: 190 nodes at depth 1-3 with a real distinct parent, so 'depth is uniformly 1' and 'children is empty on all current data' are stale in three csift comments. (2) A defect this exposes: all 32 nodes at depth 65 are `/fork` children whose parent_agent_id equals their own agent_id - a self-parent cycle that runs csift's `assign_depths` guard to its `guard > 64` cap (src/subagent/topology.rs:261), so `agents` reports depth 65 for every forked lane. Claude Code writes `parentAgentId` into 92 subagent meta.json files in 2.1.258, which is the field that would resolve a fork child's real parent; csift reads none of it (`rg -n 'parentAgentId' src/` is empty)."
},
{
"claude_code": "2.1.258",
"csift": "0.10.2",
"date": "2026-09-03",
"verdict": "drifted",
"instrument": "meta.json census over the corpus's subagent trees (agentType fork or isFork) counting parentAgentId presence, plus the spawn-index merge unit test with a repeated tool_use id",
"observed": "33 fork metas, 0 carry parentAgentId; the 0.10.1 merge was later-wins on issuer/spawns/tool_results, so a clone's copy of a sibling's spawn record would re-parent that sibling onto the clone; 0.10.2 keeps the first issuer",
"rule": "one meta.json = one fork child; a repeated tool_use id across two locals is the clone shape",
"note": "the meta-first path is the mechanism only when the harness writes parentAgentId (none of the 33 here do); the effective guard for today's clones is the self-link drop plus the first-wins fold (spawn.rs merge); unit spawn_index_merge_keeps_the_first_issuer_for_a_repeated_tool_use_id"
}
]
},
{
"id": "SUB-004",
"area": "subagents",
"behavior": "A subagent transcript's FIRST record is an `isSidechain:true` `type:\"user\"` seed carrying the spawn prompt the parent delivered - exhaustively 7467 of 7500 subagent transcripts. The one exception is a `/fork` child (33 of 7500), whose line 1 is the timestampless `fork-context-ref` record and whose next record is an `isSidechain:true` ASSISTANT record in 32 of 33 cases, so a fork child has no user seed at its head. A sidechain record does not occur in any top-level transcript: 0 lines across all 64 top-level transcripts.",
"depends": "csift deliberately does NOT gate `isSidechain` out of `is_genuine_user`, so `list`'s per-subagent preview can treat that seed as the subagent's first user message; the seed nevertheless classifies `agent.communication.inbox` with direction parent to self rather than `user.message`.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "48-52",
"snippet": " // NOTE on `isSidechain`: a subagent transcript's FIRST record is an\n // `isSidechain:true` user seed. It is NOT gated out here on purpose - `list`'s\n // per-subagent preview legitimately treats that seed as the subagent's \"first\n // user message\", and in TOP-LEVEL transcripts a sidechain seed does not occur in\n // any real corpus."
},
{
"path": "src/model/classify.rs",
"lines": "330-335",
"snippet": " // The spawn-prompt seed of a subagent transcript is an inbound comm (parent ⇨ self),\n // not the operator (GOLD §3) - unchanged, regardless of isMeta.\n if ctx.is_subagent && ctx.is_transcript_opener {\n push_unique(out, Class::CommInbox);\n return;\n }"
}
],
"instrument": "The claim's `csift list @<agent-id> --format json | jq -r .first_user` returns an OBJECT, not a string: its keys are `{excerpt, ts_local, ts_utc}`. Read `.first_user.excerpt`, and expect it EMPTY for a teammate seed (a `<teammate-message>` seed is a peer message, so `is_genuine_user` excludes it): 26 of 40 sampled subagent rows had a non-empty excerpt.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md section 3.3; SPEC.md section 1; src/model/predicates.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import glob,json,pathlib,collections; files=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/agent-*.jsonl'),recursive=True); c=collections.Counter()\\nfor p in files:\\n v=json.loads(open(p,errors='replace').readline()); m=v.get('message'); c[(v.get('type'),v.get('isSidechain'),(m or {}).get('role') if isinstance(m,dict) else None)]+=1\\nprint(len(files)); [print(n,k) for k,n in c.most_common()]\" ; python3 -c \"import glob,pathlib; tops=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*.jsonl')); print(len(tops), sum(1 for p in tops for l in open(p,errors='replace') if '\\\"isSidechain\\\":true' in l))\" ; for f in $(find ~/.claude/projects -path '*/subagents/*' -name 'agent-*.jsonl' | head -20) $(find ~/.claude/projects -path '*/subagents/workflows/*' -name 'agent-*.jsonl' | head -20); do csift show \"$f\" --line 1 --format json | jq -r 'select(.kind==\"record\")|.label'; done | sort | uniq -c",
"observed": "7500 subagent transcripts: 7467 open with ('user', isSidechain=true, message.role='user'); the other 33 open with a 'fork-context-ref' record. Top-level transcripts: 64 files, 0 lines matching \"isSidechain\":true. 40 of 40 sampled subagent line-1 records classify `agent.communication.inbox` under `csift show --line 1`, one showing `\"from\":\"team-lead\"`. Of the 33 fork children, line 2 is an ('assistant', isSidechain=true) record in 32 cases and a user sidechain seed in 1.",
"rule": "One line-1 record per subagent transcript file (exhaustive, not sampled); one grep hit per line for the top-level isSidechain scan; one label per csift kind==\"record\" row over a 40-file sample split 20 flat / 20 workflow.",
"note": "The behaviour holds with an exception the claim did not carry, and the exhaustive count (7467/7500) replaces the sampled assertion. The stated instrument is wrong twice over - `.first_user` is an object, and even read correctly it is empty for teammate lanes - so a stranger running it as written would read the claim as failing."
}
]
},
{
"id": "SUB-005",
"area": "subagents",
"behavior": "A transcript created by `/fork` opens with a TIMESTAMPLESS `type:\"fork-context-ref\"` record on LINE 1, carrying `{agentId, parentSessionId, parentLastUuid, contextLength}` - the parent's last record uuid at fork time and the number of messages carried into the fork - before any timestamped record; the field pair appears nowhere else.",
"depends": "csift's `agents` head walk captures the fork provenance for free while looking for the first timestamped record, surfacing `fork_parent_last_uuid` / `fork_context_length` and `--agent-type fork`; a timestamp-first head scan would skip line 1 and the lane's `started_utc` would be wrong.",
"code": [
{
"path": "src/subagent/lifecycle.rs",
"lines": "18-20",
"snippet": " // A `/fork` child's LINE 1 is a timestampless `fork-context-ref` record carrying\n // the parent's last uuid at fork time + the carried context length; it precedes\n // the first timestamped record, so the same head walk captures it for free."
},
{
"path": "src/subagent/lifecycle.rs",
"lines": "23-27",
"snippet": " let (head_skipped, head_consumed) = head_records(&subagent.path, |rec| {\n if rec.is_type(\"fork-context-ref\") {\n fork_parent_last_uuid = rec.parent_last_uuid.clone();\n fork_context_length = rec.context_length;\n }"
},
{
"path": "src/subagent/types.rs",
"lines": "103-107",
"snippet": " /// Fork provenance from a head `fork-context-ref` record (a `/fork` child):\n /// the parent's last record uuid at fork time. `None` for a non-fork agent.\n pub fork_parent_last_uuid: Option<String>,\n /// The context length carried into the fork, from the same record.\n pub fork_context_length: Option<u64>,"
}
],
"instrument": "`rg -l '\"type\":\"fork-context-ref\"' ~/.claude/projects --glob '*.jsonl' | while read f; do head -1 \"$f\" | jq -c '{type,parentLastUuid,contextLength,timestamp}'; done` - every hit must be line 1 with a null timestamp; then `csift agents @<session> --agent-type fork --format json | jq '{agent_id, fork_parent_last_uuid, fork_context_length}'` and `csift show @<session> --uuid <fork_parent_last_uuid>`. Counting rule: one record per forked transcript.",
"located": {
"claude_code": "2.1.237",
"csift": "0.8.0",
"source": "AGENTS.md section 3.7; SPEC.md section 6 v0.8.1 ledger; CHANGELOG 0.8.1; src/model/record.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg -l --glob '*.jsonl' '\"type\":\"fork-context-ref\"' ~/.claude/projects | wc -l ; python3 -c \"import subprocess,json,pathlib; out=subprocess.run(['rg','-l','--glob','*.jsonl','\\\"type\\\":\\\"fork-context-ref\\\"',str(pathlib.Path.home()/'.claude/projects')],capture_output=True,text=True).stdout.split()\\nline1=ts=0; keys=set(); loc={'sub':0,'top':0}\\nfor p in out:\\n loc['sub' if '/subagents/' in p else 'top']+=1\\n lines=open(p,errors='replace').readlines(); hits=[i for i,l in enumerate(lines) if '\\\"type\\\":\\\"fork-context-ref\\\"' in l]\\n line1+= hits==[0]; v=json.loads(lines[0]); keys.add(tuple(sorted(v.keys()))); ts+= v.get('timestamp') is not None\\nprint(len(out),line1,ts,loc,keys)\" ; rg --glob '*.jsonl' '\"parentLastUuid\"' ~/.claude/projects | rg -v 'fork-context-ref' | wc -l ; rg --glob '*.jsonl' '\"contextLength\"' ~/.claude/projects | rg -v 'fork-context-ref' | wc -l ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}fork-context-ref.{0,60}' ; for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --agent-type fork --format json | jq -r 'select(.kind==\"agent\")|[(if .fork_parent_last_uuid then \"HAS\" else \"NULL\" end),(.fork_context_length//\"null\"|tostring)]|@tsv'; done | sort | uniq -c",
"observed": "33 files carry a fork-context-ref record; all 33 are under `subagents/`, 0 top-level. In all 33 the record is on line 1 and ONLY line 1, and in all 33 its `timestamp` is absent. Key set is identical across all 33: ('agentId','contextLength','parentLastUuid','parentSessionId','type'). `parentLastUuid` occurs on exactly 33 lines corpus-wide and 0 of them outside a fork-context-ref line; same for `contextLength` (0 outside). Binary 2.1.258: 'async function x7n(e,n){await Fi().appendEntry({type:\"fork-context-ref\",...e},void 0,void 0,n)}' and '[fork-context-ref] parent uuid ${e.parentLastUuid} not found in ${d}'. Every csift `--agent-type fork` node reported a non-null fork_parent_last_uuid and a numeric fork_context_length (observed values 31, 117, 143, 200, 251, 344, 383, 2513, 3060, 4229, ...).",
"rule": "One record per forked transcript; the field-pair uniqueness is one grep hit per line over every *.jsonl under ~/.claude/projects, subtracting lines that also contain the fork-context-ref type literal.",
"note": "Every element of the claim was instrumented and agreed: line-1-only, timestampless, exact four-field payload, and the field pair appearing nowhere else in the corpus. The binary independently confirms the writer (`appendEntry({type:\"fork-context-ref\",...})`) and that `parentLastUuid` is looked up against the parent's record store."
}
]
},
{
"id": "SUB-006",
"area": "subagents",
"behavior": "A subagent's `agent-<id>.meta.json` companion (written by replacing the transcript's `.jsonl` suffix) carries a KIND-DEPENDENT field set. A workflow agent meta carries `{agentType}` alone (2105 of 6669) or `{agentType, spawnDepth}` (4564 of 6669). A built-in meta carries `{agentType, description, toolUseId}` plus, in current builds, some of `{spawnDepth, name, model, parentAgentId, isFork}`. A TEAMMATE meta carries `{agentType, description, name, taskKind, teamName, color, model, permissionMode, planModeRequired, spawnDepth}` (+`parentAgentId` on 8 of 164) and NO `toolUseId`, and its `agentType` is OVERLOADED with the teammate NAME rather than the real subagent type (164 of 164), which lives on the spawning `Agent` tool_use as `input.subagent_type`. Corpus key vocabulary over 7502 metas: agentType 7502, spawnDepth 5047, description 833, toolUseId 668, model 261, name 246, taskKind/teamName/color/planModeRequired/permissionMode 164 each, parentAgentId 92, isFork 33, customAgentType 1, stoppedByUser 1.",
"depends": "csift reads the meta keys tolerantly (a malformed, missing or key-absent meta yields all-`None`, never an error, and the lifecycle still resolves from the transcript) and, for a teammate only, prefers the spawn tool_use's `subagent_type` over the meta `agentType`; without that preference `agents` prints the teammate's name where its type belongs, and without a `toolUseId` the id-join spawn linkage is null.",
"code": [
{
"path": "src/subagent/meta.rs",
"lines": "54-58",
"snippet": "/// The fields csift reads from a subagent's `meta.json`. A built-in meta carries\n/// `{agentType, description, toolUseId}` (+ often `name`); a workflow agent meta carries only\n/// `{agentType}`; a TEAMMATE meta carries `{agentType, description, name, taskKind, teamName,\n/// color, model, …}` and NO `toolUseId`. All are optional - a malformed / missing / key-absent\n/// meta yields all-`None` (never an error; the lifecycle still resolves from the transcript)."
},
{
"path": "src/subagent/meta.rs",
"lines": "63-65",
"snippet": " /// The spawning parent `Task`/`Agent` tool_use id (built-in only; the topology join\n /// key). Captured here so the previously-dropped `toolUseId` reaches the topology.\n pub tool_use_id: Option<String>,"
},
{
"path": "src/subagent/meta.rs",
"lines": "67-72",
"snippet": " /// `taskKind` - `\"in_process_teammate\"` marks a teammate (the only way to distinguish it\n /// from a built-in Task subagent, since both share the on-disk location). `None`/other for\n /// a plain built-in or workflow agent.\n pub task_kind: Option<String>,\n /// `teamName` - the team a teammate belongs to (teammate metas only).\n pub team_name: Option<String>,"
},
{
"path": "src/subagent/spawn.rs",
"lines": "20-22",
"snippet": " /// `input.subagent_type` on the spawning tool_use - the richer agent-type label used\n /// as a fallback when the built-in meta.json's `agentType` is absent.\n pub subagent_type: Option<String>,"
}
],
"instrument": "`for f in ~/.claude/projects/*/*/subagents/*.meta.json; do jq -c 'keys' \"$f\"; done | sort | uniq -c` - expect the three key sets above; `jq -r 'select(.taskKind==\"in_process_teammate\") | [.agentType,.name] | @tsv'` over the same files must show agentType equal to name; then `csift agents @<session> --format json | jq -r 'select(.shape==\"teammate\") | [.agent_type, .name] | @tsv'` must report a real subagent type, not the name. Counting rule: one key set per meta file.",
"located": {
"claude_code": "2.1.191",
"csift": "0.3.0",
"source": "AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5; src/subagent/meta.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import glob,json,pathlib,collections; metas=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/*.meta.json'),recursive=True)\\nflat=[p for p in metas if '/workflows/' not in p]; wf=[p for p in metas if '/workflows/' in p]\\nfor label,ps in (('flat',flat),('workflows',wf)):\\n c=collections.Counter(); tm=eq=0\\n for p in ps:\\n v=json.load(open(p)); c[tuple(sorted(v.keys()))]+=1\\n if v.get('taskKind')=='in_process_teammate': tm+=1; eq+= v.get('agentType')==v.get('name')\\n print(label,len(ps)); [print(' ',n,list(k)) for k,n in c.most_common(8)]; print(' teammates',tm,'agentType==name',eq)\" ; for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --shape teammate --format json | jq -r 'select(.kind==\"agent\")|[(if .agent_type==.name then \"SAME\" else \"DIFF\" end),(if .spawn_tool_use_id then \"SPAWN\" else \"NOSPAWN\" end)]|@tsv'; done | sort | uniq -c ; rg -n 'parentAgentId|spawnDepth|isFork' src/",
"observed": "7502 meta files: 833 at the flat built-in location, 6669 under workflows/. Workflow metas now carry TWO key sets: ['agentType','spawnDepth'] x4564 and ['agentType'] x2105. Flat metas: ['agentType','description','toolUseId'] x294, +spawnDepth x198, ['agentType','color','description','model','name','permissionMode','planModeRequired','spawnDepth','taskKind','teamName'] x155, ['agentType','description','model','parentAgentId','spawnDepth','toolUseId'] x79, ['agentType','description','name','toolUseId'] x56, isFork forms x14 and x11, teammate+parentAgentId x8. Teammates: 164, agentType == name in 164 of 164, and NONE carries toolUseId. csift teammate rows: 146 report an agent_type DIFFERENT from the name with a recovered spawn id, 9 SAME with a spawn id, 9 SAME without one. `rg -n 'parentAgentId|spawnDepth|isFork' src/` returns nothing.",
"rule": "One key set per meta file, partitioned by whether the path contains /workflows/; one row per csift kind==\"agent\" teammate row for the type-recovery census.",
"note": "The kind-dependence and the teammate agentType overload both hold exactly (164/164). Refined because current Claude Code writes more than the claim lists: a workflow meta now usually carries `spawnDepth` too (4564 of 6669, so 'only {agentType}' covers just 32% of them), and four fields csift reads none of - `spawnDepth` (5047 metas), `parentAgentId` (92), `isFork` (33), `model` (261). csift's type-preference works: 146 of 164 teammate rows report a real subagent type distinct from the name; the 18 that fall back to the name are lanes where the name-join found no spawn record or the spawn carried the same string."
}
]
},
{
"id": "SUB-007",
"area": "subagents",
"behavior": "A subagent's `meta.json` carries NO `status` field (0 of 7502 metas, up from the 400 originally sampled) and is close to write-once: the only post-spawn mutation observed on one is a `\"stoppedByUser\":true` write (1 of 7502), and the workflow RESULT manifests under the session's `workflows/` dir are terminal-only (1 live run dir had no manifest at scan time).",
"depends": "csift never derives child liveness from meta or from a result file - `status`/`wait` read each child's own transcript tail plus the incremental workflow journal - so a status field appearing in meta would be a cheaper instrument csift is currently forgoing, and any code assuming one would read `None` forever.",
"code": [
{
"path": "src/live/children.rs",
"lines": "3-6",
"snippet": "//! `subagents/*.meta.json` has NO status field (child liveness can never come from\n//! meta), and workflow RESULT files are terminal-only - but `journal.jsonl` inside each\n//! `wf_*` dir is written INCREMENTALLY (`{type:\"started\",agentId}` at spawn,\n//! `{type:\"result\",agentId}` at return), so `started - result` = workflow agents in"
}
],
"instrument": "`python3 -c \"import json,glob,pathlib;ps=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/*.meta.json'),recursive=True);print(sum('status' in json.load(open(p)) for p in ps), len(ps))\"` - expect 0 of N; flattening the keys the same way shows `stoppedByUser` on a rare few. Counting rule: one presence test per meta file.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "AGENTS.md section 3.7; SPEC.md section 6.13; src/live/children.rs module doc; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import glob,json,pathlib,collections; ps=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/*.meta.json'),recursive=True); keys=collections.Counter()\\nst=sb=0\\nfor p in ps:\\n v=json.load(open(p)); keys.update(v.keys()); st+= 'status' in v; sb+= 'stoppedByUser' in v\\nprint(len(ps),st,sb); print(dict(keys.most_common()))\" ; python3 -c \"import glob,json,os,pathlib; sess=sorted({os.path.dirname(os.path.dirname(p)) for p in glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json'))}); m=d=0\\nfor sd in sess:\\n man={os.path.basename(p)[:-5] for p in glob.glob(os.path.join(sd,'workflows','wf_*.json'))}; dirs={os.path.basename(p) for p in glob.glob(os.path.join(sd,'subagents','workflows','wf_*'))}; m+=len(man-dirs); d+=len(dirs-man)\\nprint(m,d)\" ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,50}stoppedByUser.{0,40}'",
"observed": "7502 meta files: 0 carry a `status` key; 1 carries `stoppedByUser`. Full key vocabulary contains no status-like field other than that single `stoppedByUser`. Run-manifest side: of 163 workflow run manifests, 4 have no corresponding `subagents/workflows/wf_<id>/` agent dir and 1 agent dir has no manifest - consistent with a manifest written only at run end. Binary 2.1.258 carries the meta-writer fragment '...d?.stoppedByUser&&{stoppedByUser:!0},...d?.parentAgentId' and a separate liveness test 'e.stoppedByUser!==!0&&(e.status===\"running\"||e.status===' on a different record type.",
"rule": "One presence test per meta file for each key; one set-difference row per workflow run id for the manifest/agent-dir comparison.",
"note": "The load-bearing half - no status field in meta, so child liveness can never come from it - holds at 18x the original sample size (0 of 7502). Refined on the numbers and on 'effectively write-once': the 2.1.258 meta writer merges `stoppedByUser` and `parentAgentId` conditionally into the object it writes, and 92 metas carry `parentAgentId`, so a second mutable field exists even though the corpus cannot distinguish a spawn-time write from a later one. A status-bearing sibling record does exist in the binary (`e.status===\"running\"`), but on a different object than the subagent meta."
}
]
},
{
"id": "SUB-008",
"area": "subagents",
"behavior": "Each workflow run's `journal.jsonl` is written INCREMENTALLY, one line per lifecycle event - `{type:\"started\", key, agentId}` at spawn and `{type:\"result\", key, agentId, result}` at return - so `started` minus `result` approximates that run's agents in flight; the two counts do not balance while agents are running (36 of 160 journals measured with started != result), which a terminal dump could never produce. The event-type set is OPEN, not a pair: a third terminal type `failed` (same key shape as `started`) also occurs (1 of 12718 events), so `started - result` overstates in-flight by the number of failed agents.",
"depends": "csift's `JournalCache` parses each journal once per topology build and keys completion on the presence of a `result` event for an `agentId` (first event per agent wins), feeding `agents`' workflow status and returned message and `status`'s `journal_in_flight`, which is what produces a waiting-children verdict for a fan-out whose child transcripts already look settled.",
"code": [
{
"path": "src/live/children.rs",
"lines": "122-132",
"snippet": " for line in raw.lines() {\n let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {\n continue;\n };\n match v.get(\"type\").and_then(serde_json::Value::as_str) {\n Some(\"started\") => started += 1,\n Some(\"result\") => resulted += 1,\n _ => {}\n }\n }\n report.journal_in_flight += started.saturating_sub(resulted);"
},
{
"path": "src/subagent/meta.rs",
"lines": "166-177",
"snippet": " if v.get(\"type\").and_then(serde_json::Value::as_str) != Some(\"result\") {\n continue;\n }\n let Some(agent) = v.get(\"agentId\").and_then(serde_json::Value::as_str) else {\n continue;\n };\n let payload = match v.get(\"result\") {\n Some(serde_json::Value::String(s)) => Some(s.clone()),\n Some(other) => Some(other.to_string()),\n None => None,\n };\n // FIRST event per agent wins - the former scans returned on first match."
}
],
"instrument": "`for j in ~/.claude/projects/*/*/subagents/workflows/*/journal.jsonl; do s=$(grep -c '\"type\":\"started\"' \"$j\"); r=$(grep -c '\"type\":\"result\"' \"$j\"); echo \"$s $r\"; done | awk '$1!=$2' | wc -l` - expect a nonzero minority; compare the same difference with `csift status @<session> --format json | jq '.evidence[] | select(.surface==\"children\")'` during a live fan-out. Counting rule: whole-line type matches per journal file, started minus result per run.",
"located": {
"claude_code": "2.1.237",
"csift": "0.6.0",
"source": "AGENTS.md section 3.7; SPEC.md section 6.13; src/live/children.rs module doc; CHANGELOG 0.9.0; dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import glob,json,pathlib,collections; js=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/workflows/wf_*/journal.jsonl')); types=collections.Counter(); sk=collections.Counter(); rk=collections.Counter(); imb=bal=ts=tr=0\\nfor p in js:\\n s=r=0\\n for line in open(p,errors='replace'):\\n v=json.loads(line); t=v.get('type'); types[t]+=1\\n if t=='started': s+=1; sk[tuple(sorted(v.keys()))]+=1\\n elif t=='result': r+=1; rk[tuple(sorted(v.keys()))]+=1\\n ts+=s; tr+=r; imb+= s!=r; bal+= s==r\\nprint(len(js),bal,imb,ts,tr,dict(types),dict(sk),dict(rk))\" ; find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 rg -l '\"type\":\"failed\"' | while read j; do echo \"started=$(rg -c '\\\"type\\\":\\\"started\\\"' \"$j\") result=$(rg -c '\\\"type\\\":\\\"result\\\"' \"$j\") failed=$(rg -c '\\\"type\\\":\\\"failed\\\"' \"$j\")\"; done ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,50}journal\\.jsonl.{0,60}'",
"observed": "160 journals; 124 with started == result, 36 with started != result. Totals: 6669 started, 6048 result, and a THIRD event type not in the claim - 1 'failed'. started key set is uniformly ('agentId','key','type'); result key set is uniformly ('agentId','key','result','type'). The single journal carrying a failed event reads started=107 result=106 failed=1, so its entire imbalance is that one terminal failure. Binary 2.1.258: 'Per-agent results: ${d}/journal.jsonl \\u2014 one {\"type\":\"result\",...} line per complet' and 'Read <transcriptDir>/journal.jsonl \\u2014 it records each agent's actual return valu'.",
"rule": "Whole-line JSON parse per journal line, bucketed by the `type` value; started minus result computed per run file; one imbalance row per journal file.",
"note": "Incremental writing and the live imbalance both hold, at 160 journals rather than 153. Refined because the event vocabulary is open: a `failed` event terminates an agent without a `result` line, and csift's counter (src/live/children.rs:126-129) matches only \"started\" and \"result\" with a catch-all `_ => {}`, so a failed workflow agent inflates `journal_in_flight` by 1 permanently. The one journal on disk carrying a failed event is exactly the case: started 107, result 106, failed 1."
}
]
},
{
"id": "SUB-009",
"area": "subagents",
"behavior": "Workflow RUN manifests are written at the TOP level of the session sidecar as `<session-uuid>/workflows/wf_<id>.json` - a different directory from `<session-uuid>/subagents/workflows/wf_<id>/`, which holds no `wf_*.json` at all - carrying 18 to 20 keys: `{runId, taskId, workflowName, status, agentCount, durationMs, totalTokens, totalToolCalls, defaultModel, startTime, timestamp, script, scriptPath, logs, phases, result, summary, workflowProgress}` plus `args` and `error` when present. There is no `startedAt` field; the run's start instant is `startTime`.",
"depends": "csift's `discover_workflow_runs` reads those manifests as the `kind:\"run\"` rows of `agents` and parents each `wf_<id>` agent under its run by `workflow_id == runId`; looking under `subagents/workflows/` alone would find agent dirs and no run metadata, and the `workflows/scripts/` subdir plus any non-`wf_*.json` entry are ignored.",
"code": [
{
"path": "src/subagent/topology.rs",
"lines": "157-160",
"snippet": "/// Read every top-level `<session>/workflows/wf_*.json` manifest (§5) as a [`WorkflowRun`].\n/// Returns an empty vec when the session has no sidecar / no `workflows/` dir (never an\n/// error for the common no-workflow case). The `workflows/scripts/` subdir and any\n/// non-`wf_*.json` entry are ignored."
},
{
"path": "src/subagent/tests.rs",
"lines": "49",
"snippet": "/// A top-level `workflows/wf_abc.json` manifest is also written (NOT under"
}
],
"instrument": "`csift agents ... | jq 'select(.kind==\"run\")'` does NOT emit one row per manifest. A row is emitted only for a run that has a manifest AND at least one in-scope agent node, and csift synthesizes an extra row with `status: null` for a `subagents/workflows/wf_<id>/` dir that has no manifest yet. Measured: 163 manifests, 4 with no agent dir, 1 agent dir with no manifest, and 163 - 4 + 1 = 160 rows emitted - an exact reconciliation. Target each session by its explicit `<uuid>.jsonl` path; an `@<uuid>` target can resolve to two project dirs and double-count.",
"located": {
"claude_code": "2.1.237",
"csift": "0.2.0",
"source": "AGENTS.md section 3.7; SPEC.md section 6.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "find ~/.claude/projects -path '*/workflows/*' -name 'wf_*.json' ! -path '*/subagents/*' | wc -l ; find ~/.claude/projects -path '*/subagents/workflows/*' -name 'wf_*.json' | wc -l ; python3 -c \"import glob,json,pathlib,collections; ms=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json')); c=collections.Counter(); [c.update([tuple(sorted(json.load(open(p)).keys()))]) for p in ms]; print(len(ms)); [print(n,list(k)) for k,n in c.most_common()]\" ; python3 -c \"import glob,json,os,pathlib,subprocess,collections; sess=sorted({os.path.dirname(os.path.dirname(p)) for p in glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json'))}); disk=collections.Counter(); csi=collections.Counter()\\nfor sd in sess:\\n for p in glob.glob(os.path.join(sd,'workflows','wf_*.json')): disk[json.load(open(p)).get('status')]+=1\\n r=subprocess.run(['csift','agents',sd+'.jsonl','--format','json'],capture_output=True,text=True)\\n for line in r.stdout.splitlines():\\n v=json.loads(line)\\n if v.get('kind')=='run': csi[v.get('status')]+=1\\nprint(dict(disk),dict(csi))\"",
"observed": "163 manifests at `<session-uuid>/workflows/wf_<id>.json`; 0 `wf_*.json` files anywhere under `subagents/workflows/`. Manifest key sets are 18 to 20 keys, never 10: the majority (111 files) is ['agentCount','defaultModel','durationMs','logs','phases','result','runId','script','scriptPath','startTime','status','summary','taskId','timestamp','totalTokens','totalToolCalls','workflowName','workflowProgress'], plus 'args' (43 files) and plus 'error' (9 files). There is NO `startedAt` key. The same `workflows/` dir also holds a `scripts/` subdir (14 sessions) and per-run `.js` files whose names do not start with `wf_`. csift emits a 13-key `kind:\"run\"` row: ['agent_count','default_model','duration_ms','kind','run_id','session_id','started_local','started_utc','status','task_id','total_tokens','total_tool_calls','workflow_name'], 160 rows against 163 manifests.",
"rule": "One key set per manifest file; one `kind:\"run\"` row per csift invocation, targeting each session by its explicit transcript path so a uuid cannot resolve to two project dirs.",
"note": "The location claim - the discriminating fact - holds exactly (163 manifests at the top-level workflows dir, 0 under subagents/workflows). Refined on the field list: one named field (`startedAt`) does not exist and the manifest is roughly twice as wide as listed. The stated instrument also needed correcting; run through it as written, a stranger would see 160 rows against 163 manifests and read that as a defect rather than the documented run-must-have-agents filter at src/agents/json.rs:39-47 plus the synthesized stand-in at src/agents/run.rs:109-131."
}
]
},
{
"id": "SUB-010",
"area": "subagents",
"behavior": "A workflow RUN's `status` is written verbatim by the workflow runtime into the run MANIFEST and is an OPEN set, not a closed enum: `completed`, `killed` and `failed` all observed (154 / 4 / 5 over 163 manifests). It is a different axis from the per-agent lifecycle status csift computes from a transcript tail. The journal does NOT carry a run status - a journal line's only discriminator is `type`, whose observed values are `started`, `result` and `failed`.",
"depends": "csift reads the value with a plain string field read and prints it unchanged on `kind:\"run\"` rows; tightening it into a closed set would drop or mislabel a run state the runtime later adds.",
"code": [
{
"path": "src/subagent/meta.rs",
"lines": "113-117",
"snippet": "pub(crate) fn journal_reports_completion(subagent: &Subagent, journals: &JournalCache) -> bool {\n journals\n .events_for(subagent)\n .is_some_and(|data| data.results.contains_key(&subagent.agent_id))\n}"
}
],
"instrument": "The manifest status census and the csift run-row census agree only over runs that are both manifest-backed and agent-bearing; compare them after subtracting manifests with no `subagents/workflows/wf_<id>/` dir (4) and adding csift's synthesized null-status rows for agent dirs with no manifest (1). Target each session by explicit transcript path, not `@<uuid>`.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 -c \"import glob,json,pathlib,collections; ms=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json')); c=collections.Counter(json.load(open(p)).get('status') for p in ms); print(len(ms),dict(c))\" ; python3 -c \"import glob,json,os,pathlib,subprocess,collections; sess=sorted({os.path.dirname(os.path.dirname(p)) for p in glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json'))}); csi=collections.Counter()\\nfor sd in sess:\\n r=subprocess.run(['csift','agents',sd+'.jsonl','--format','json'],capture_output=True,text=True)\\n for line in r.stdout.splitlines():\\n v=json.loads(line)\\n if v.get('kind')=='run': csi[v.get('status')]+=1\\nprint(dict(csi))\" ; find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 cat | jq -c 'keys' | sort | uniq -c",
"observed": "163 manifests: status completed 154, killed 4, failed 5 - three distinct values, no fourth. csift `kind:\"run\"` rows over the same 15 sessions: completed 153, killed 4, failed 2, null 1 (160 rows), which reconciles exactly against the 4 manifests with no agent dir and the 1 agent dir with no manifest. Journal lines carry only two key sets, ['agentId','key','type'] and ['agentId','key','result','type'] - there is no `status` key anywhere in any journal.",
"rule": "One status per manifest file; one status per csift kind==\"run\" row, sessions targeted by explicit transcript path; one key set per journal line for the journal-status check.",
"note": "The open-set finding holds and the same three values recur at a larger N (154/4/5 over 163, against the ledger's 148/5/4 over 157) - nothing tightened, nothing new appeared. Refined on two points: the status is a MANIFEST field only (the journal has no status key at all, so 'in the run manifest and the journal' is wrong), and the stated instrument's 'the two must agree' needs the manifest/agent-dir reconciliation or it reads as a mismatch."
}
]
},
{
"id": "SUB-011",
"area": "subagents",
"behavior": "The tools that spawn a subagent are named exactly `Task`, `Agent` and `Workflow` in a tool_use `name` field - all three are registered tool names in Claude Code 2.1.258. `Agent` and `Workflow` are the spellings a real corpus carries (corpus-wide tool-axis census: Agent 1832 records, Workflow 356 records, across 35 distinct tool keys); `Task` is matched defensively and occurred zero times in the corpus measured here.",
"depends": "csift's global spawn index and the self-to-child comm direction gate on that three-name set; matching only `Task` leaves every child unlinked and collapses the `agents` tree to a flat list, and a fourth spawn tool name would leave its children parentless and unlabelled under `agent.communication.signal`.",
"code": [
{
"path": "src/model/peer.rs",
"lines": "262-264",
"snippet": "pub(crate) fn is_spawn_tool_name(name: &str) -> bool {\n matches!(name, \"Task\" | \"Agent\" | \"Workflow\")\n}"
},
{
"path": "src/subagent/spawn.rs",
"lines": "250-252",
"snippet": "pub(crate) fn is_spawn_tool(name: &str) -> bool {\n matches!(name, \"Agent\" | \"Task\" | \"Workflow\")\n}"
}
],
"instrument": "`csift search '' @<session> -t agent.tool.use --count-by tool --format json | jq -r 'select(.key==\"Agent\" or .key==\"Task\" or .key==\"Workflow\") | [.key,.records] | @tsv'`. Counting rule: one record per tool_use record on the tool axis, deduped per record by the census.",
"located": {
"claude_code": "2.1.191",
"csift": "0.6.0",
"source": "AGENTS.md section 3.7; SPEC.md section 10.3"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '\"Bash\",\"BashOutput\"[^\\]]*' AND csift search '' --count-by tool --format json AND csift search '' -t agent.communication.sent --count-by tool --format json",
"observed": "Binary tool registry string (verbatim excerpt): \"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",\"Edit\",...,\"Agent\",\"Task\",\"Workflow\",\"Skill\",... - all three spawn names are registered tools at 2.1.258. Corpus tool-axis census over 653288 matched records / 35 distinct tool keys: Agent 1832, Workflow 356, SendMessage 1581, TaskCreate 2222, TaskStop 182; the key `Task` is ABSENT (zero records). Restricted to the sent-comm view: Agent 916, Workflow 178, SendMessage 725, no Task.",
"rule": "One census record per record on the tool axis (a tool_use and its paired tool_result each count once, hence the exact 2x between the unfiltered and the single-view counts); a tool name absent from the 35 keys occurred zero times in the whole projects root.",
"note": "The three-name set is confirmed twice over: at the binary level (all three appear in 2.1.258's tool registry string) and at the corpus level (Agent and Workflow both occur; Task does not). The claim's per-transcript figures (151 Agent, 22 Workflow in 'one representative parent transcript') are not a reproducible counting rule - they name no transcript and transcripts grow, so they were replaced with a corpus-wide census a stranger can rerun. csift's defensive match on `Task` is the right call: the name is live in the harness even though this corpus never exercised it."
}
]
},
{
"id": "SUB-012",
"area": "subagents",
"behavior": "A TEAMMATE subagent (Claude Code's persistent, directly addressable team-member agent) lands at the BUILT-IN on-disk location `subagents/agent-<id>.jsonl`, so location alone cannot classify it; the sole discriminator is its `meta.json` `\"taskKind\":\"in_process_teammate\"`, which workflow agents never carry.",
"depends": "csift upgrades the location-derived `BuiltinTask` kind to `Teammate` on that literal, and the upgrade is what `agents --shape teammate`, the teammate control hint, the name-join spawn linkage and the real-agent-type preference all key on.",
"code": [
{
"path": "src/subagent/types.rs",
"lines": "13-16",
"snippet": " /// A \"teammate\" (`taskKind:\"in_process_teammate\"`) - Claude Code's persistent, directly\n /// addressable team-member agent. It lands at the built-in on-disk LOCATION\n /// (`subagents/agent-<id>.jsonl`), so location alone can't tell it apart; the discriminator\n /// is the meta.json `taskKind`. Its canonical id embeds the teammate name"
},
{
"path": "src/subagent/meta.rs",
"lines": "28-35",
"snippet": " // A built-in-LOCATION agent whose meta declares `taskKind:\"in_process_teammate\"` is a\n // teammate, not a plain Task subagent - the only way to tell them apart (both sit at\n // `subagents/agent-<id>.jsonl`). Workflow agents never carry this taskKind, so the upgrade\n // only ever fires from BuiltinTask.\n let kind = if kind == SubagentKind::BuiltinTask\n && meta.task_kind.as_deref() == Some(\"in_process_teammate\")\n {\n SubagentKind::Teammate"
}
],
"instrument": "`rg -l '\"taskKind\":\"in_process_teammate\"' ~/.claude/projects/*/*/subagents/*.meta.json | wc -l`, confirming each such meta sits directly under `subagents/` and never under `workflows/`; the same set must be what `csift agents @<session> --shape teammate --format json` lists. Counting rule: one row per meta file carrying the literal.",
"located": {
"claude_code": "2.1.191",
"csift": "0.3.0",
"source": "AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5; src/subagent/meta.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'in_process_teammate' AND a python3 pass over the 7500 files matching ~/.claude/projects/*/*/subagents/*.meta.json and ~/.claude/projects/*/*/subagents/workflows/*/*.meta.json, tallying json['taskKind'] and the key union",
"observed": "Binary carries the literal `in_process_teammate` at 2.1.258. Of 7500 meta.json files: taskKind = {'<absent>': 7336, 'in_process_teammate': 164}. All 164 teammate metas sit directly under `subagents/`; a repeat of the same tally restricted to the `subagents/workflows/*/` glob returned 0. Teammate meta key set: agentType, color, description, model, name, permissionMode, planModeRequired, spawnDepth, taskKind, teamName - and 0 of 164 carry `toolUseId`.",
"rule": "One row per meta.json file; a file counts as a teammate iff its parsed `taskKind` is exactly `in_process_teammate`. Location is decided by which glob matched the file.",
"note": "Fully confirmed, and the location-ambiguity premise is confirmed too: all 164 teammate metas share the built-in location with the 669 non-teammate built-in metas, so nothing but `taskKind` separates them. The corpus also confirms the companion fact csift relies on downstream - `toolUseId` is absent on 164/164 teammate metas, which is why the id-join cannot reach a teammate's spawn (see SUB-014)."
}
]
},
{
"id": "SUB-013",
"area": "subagents",
"behavior": "A teammate's canonical agent id EMBEDS its name rather than being a bare hex: the shape is `a<Name>-<16 hex>`, and the name half may itself contain dashes.",
"depends": "csift's `@<agent-id>` entry gate is `is_bare_subagent_hex || is_teammate_agent_id` (with an explicit uuid guard so an `a`-led uuid cannot slip in), so every id `agents` prints round-trips as a target; a hex-only gate sent teammate ids down the project-dir branch, where they failed.",
"code": [
{
"path": "src/path/ids.rs",
"lines": "45-57",
"snippet": "pub(crate) fn is_teammate_agent_id(s: &str) -> bool {\n if is_uuid(s) {\n return false;\n }\n let Some((head, tail)) = s.rsplit_once('-') else {\n return false;\n };\n head.len() >= 2\n && head.starts_with('a')\n && head.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')\n && tail.len() >= 12\n && tail.bytes().all(|b| b.is_ascii_hexdigit())\n}"
}
],
"instrument": "`ls ~/.claude/projects/*/*/subagents/agent-a*-*.jsonl | sed 's|.*/agent-||;s|.jsonl||'` - every id must match `^a[A-Za-z0-9-]+-[0-9a-f]{16}$` and its sibling meta must carry the teammate taskKind; then feed each id printed by `csift agents @<session> --shape teammate --format json | jq -r .agent_id` back as `csift show @<id> --turn -1` and require a fetch. Counting rule: teammate nodes that round-trip as targets over teammate nodes listed.",
"located": {
"claude_code": "2.1.191",
"csift": "0.3.0",
"source": "AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 regex check `^a[A-Za-z0-9-]+-[0-9a-f]{16}$` over the basenames of every ~/.claude/projects/*/*/subagents/agent-*.meta.json whose taskKind is in_process_teammate AND csift agents @<session-uuid> --shape teammate --format json | jq -r 'select(.kind==\"agent\").agent_id' piped one id at a time into csift show @<agent-id> --turn -1",
"observed": "164/164 teammate ids match `^a[A-Za-z0-9-]+-[0-9a-f]{16}$`; hex-tail length is 16 on 164/164; 135/164 have at least one dash inside the NAME half; the name half equals the meta `name` on 164/164. Round-trip on one session's 49 teammate ids: ok=49 fail=0 (every `csift show @<agent-id> --turn -1` exited 0 and fetched records).",
"rule": "One row per teammate meta file for the shape test; one attempt per agent_id csift printed for the round-trip, counting a non-zero exit or empty fetch as a failure.",
"note": "Both halves of the claim measured: the shape (`a` + name + `-` + 16 hex, name half dash-tolerant) and the consequence csift's `is_teammate_agent_id` gate exists for (every printed id is a usable `@` target). The dash-in-name case is not an edge case here - it is the majority, 135 of 164."
}
]
},
{
"id": "SUB-014",
"area": "subagents",
"behavior": "A teammate is spawned by an `Agent` tool_use whose `input.name` equals the teammate name and whose `input.subagent_type` carries the real agent type (distinct from the meta's `agentType`, which is overloaded with the teammate NAME - measured equal to `name` on 164/164 teammate metas). Because the teammate meta carries no `toolUseId` (0/164), that NAME is the only join back to the spawning record - but the join is not always available: in the measured session 41 of 49 teammates joined a spawn by name (8 teammate names never appear as a spawn `input.name`), and 3 of 52 `Agent` spawns carry neither `name` nor `subagent_type`. The team lead subsequently addresses the teammate through `<teammate-message>` blocks and the SendMessage tool.",
"depends": "csift keeps a `spawn input.name -> [(trigger_utc, tool_use_id)]` index and disambiguates a recurring name by trigger time, recovering the spawn tool_use id, the true trigger instant, the parent agent and the real subagent type; without the name-join a teammate node has null spawn linkage, no trigger timestamp and floats at depth 0.",
"code": [
{
"path": "src/subagent/spawn.rs",
"lines": "38-42",
"snippet": " /// `spawn input.name → [(trigger_utc, tool_use_id)]` for every spawn tool_use that named\n /// its agent. The NAME-join fallback for a TEAMMATE, whose meta carries no `toolUseId` (so\n /// the usual id-join can't reach its spawning `Agent` tool_use). Keyed by the `Agent` tool's\n /// `name` param (== the teammate's meta `name`). A name may recur across a session, so the\n /// values are a list disambiguated by trigger time in [`Self::spawn_id_for_name`]."
}
],
"instrument": "`csift search '\"name\":\"Agent\"' @<session> -t agent.tool.use --raw | jq -c '.message.content[]?|select(.name==\"Agent\")|.input|{name,subagent_type}'` - `input.name` must equal a teammate meta's `name` while `input.subagent_type` differs from that meta's `agentType`; `csift agents @<session> --shape teammate --format json | jq '{agent_id, agent_type, name, team_name, spawn_tool_use_id}'` must show the recovered linkage. Counting rule: one row per `Agent` tool_use block.",
"located": {
"claude_code": "2.1.191",
"csift": "0.5.0",
"source": "AGENTS.md section 3.7; SPEC.md section 6.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 pass over ONE main transcript (~/.claude/projects/<encoded>/<session-uuid>.jsonl) collecting every tool_use block with name==\"Agent\" and its input.name / input.subagent_type AND a python3 pass over that session's teammate metas for (name, agentType) AND csift agents @<session-uuid> --shape teammate --format json",
"observed": "52 `Agent` tool_use blocks in that transcript; 49 carry input.name, and all 49 of those also carry input.subagent_type (values: claude 48, opus 1; 3 blocks carry neither). The session's 49 teammate metas have agentType == their own `name` on 49/49, while the spawn's subagent_type is `claude`/`opus` - so the two really are different strings. Meta names present as a spawn input.name: 41/49. csift's recovered nodes: 49 teammate rows, spawn_tool_use_id non-null on 41/49, trigger_utc non-null on 49/49, agent_type rendered as `claude` (the spawn's subagent_type) rather than the meta's name-overloaded agentType.",
"rule": "One row per `Agent` tool_use block for the spawn side; one row per teammate meta file for the meta side; a name-join counts as recovered iff csift's node emits a non-null spawn_tool_use_id.",
"note": "The mechanism is confirmed; the coverage is the correction. A teammate whose name never appears in a spawn `input.name` gets a null spawn_tool_use_id, so a consumer must not treat spawn linkage as guaranteed. csift still dates all 49 nodes (trigger_utc non-null on 49/49), so the missing join costs the spawn id, not the timeline. Same session shows 116 `<teammate-message>` occurrences and 43 SendMessage tool_use blocks, so the addressing half of the claim is exercised there too."
}
]
},
{
"id": "SUB-015",
"area": "subagents",
"behavior": "A PERSISTENT teammate spawn's tool_result is an immediate acknowledgement carrying `toolUseResult.status == \"teammate_spawned\"`; the teammate's actual work returns later as inbound `<teammate-message>` records, never through that tool_result.",
"depends": "the ack shares the spawn `tool_use_id`, so the spawn lookup would otherwise resolve it as a return - csift labels it `agent.tool.result` ONLY, never `agent.communication.inbox`, and gives it no comm direction, so a count of inbound replies counts real replies rather than launch confirmations.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "62-68",
"snippet": " pub(crate) fn is_teammate_spawn_ack(&self) -> bool {\n self.tur_probe()\n .as_ref()\n .and_then(|p| p.status.as_ref())\n .and_then(serde_json::Value::as_str)\n == Some(\"teammate_spawned\")\n }"
}
],
"instrument": "The ledger's instrument does not work: `csift search 'teammate_spawned' <project dir> --raw` matches RENDERED record text, and `toolUseResult` is never rendered, so the pattern only finds prose that mentions the literal (corpus-wide it returned 86 records, 36 with a dict toolUseResult, and `.toolUseResult.status` was null on all 36). Use a direct pass over the transcript instead: for each line, parse `toolUseResult` and keep those whose `status` is `teammate_spawned`, then join `tool_use_id` back to the spawning tool_use in the same file. csift's labelling half of the check works as written (`csift show @<session-uuid> --line <n> --format json`).",
"located": {
"claude_code": "2.1.191",
"csift": "0.6.0",
"source": "AGENTS.md section 3.3a; src/model/classify.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 pass over all 11 main transcripts that own at least one teammate meta, joining every tool_result whose toolUseResult.status == \"teammate_spawned\" back to the tool_use with the same id in the same file AND csift show @<session-uuid> --line <n> --format json | jq -r '.labels[]' on two such records AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'teammate_spawned'",
"observed": "156 acks across 11 transcripts; toolUseResult.status == 'teammate_spawned' on 156/156; 156/156 join a tool_use named `Agent` in the same file, 0 join any other tool. Ack toolUseResult key set (156 each): status, prompt, teammate_id, agent_id, model, name, color, tmux_session_name, tmux_window_name, tmux_pane_id, team_name, is_splitpane, plan_mode_required (agent_type on 146). The carriers are type:\"user\"/role:\"user\" records. csift labels on two sampled acks: ['agent.tool.result'] - single label, direction null. Binary carries the literal `teammate_spawned` at 2.1.258.",
"rule": "One row per tool_result carrier whose toolUseResult.status is exactly `teammate_spawned`; a join counts only when the ack's tool_use_id matches a tool_use id in the SAME transcript.",
"note": "The behavior is confirmed exactly as stated, including the consequence csift depends on: the ack shares the spawn's tool_use_id (156/156) yet carries only `agent.tool.result`, never `agent.communication.inbox`, and no direction - so counting inbound replies does not count launch confirmations. Only the ledger's suggested instrument needed replacing. Incidental observation worth recording: every ack in this corpus carries tmux_* fields with is_splitpane false, i.e. the pane id names the orchestrator's own pane, not a pane of the teammate's own."
}
]
},
{
"id": "SUB-016",
"area": "subagents",
"behavior": "An ASYNC / background `Agent` spawn's tool_result is an immediate LAUNCH ACK rather than the child's work: it carries `toolUseResult` {isAsync:true, status:\"async_launched\", agentId, description, outputFile} and a text body opening `Async agent launched successfully`, and it shares the spawn's `tool_use_id`. At 2.1.258 that opening sentence continues with a harness instruction not to quote the tool result or the agentId into a user-facing reply, so the ack must be matched as a PREFIX, never as a whole-string equality.",
"depends": "csift keeps the ack labelled `agent.tool.result` only (never `agent.communication.inbox`) and redirects returned-message resolution to the child transcript tail; without that, `agents` reports the launch confirmation as the agent's report.",
"code": [
{
"path": "src/model/classify.rs",
"lines": "82-88",
"snippet": " if let Some(probe) = self.tur_probe() {\n if probe.status.as_ref().and_then(serde_json::Value::as_str) == Some(\"async_launched\")\n || probe.is_async.as_ref().and_then(serde_json::Value::as_bool) == Some(true)\n {\n return true;\n }\n }"
},
{
"path": "src/model/markers.rs",
"lines": "185",
"snippet": "pub const ASYNC_LAUNCH_ACK_PREFIX: &str = \"Async agent launched successfully\";"
}
],
"instrument": "Match the rendered prefix, not the structured field: `csift search 'Async agent launched' --raw --max-count 0` reaches these records (search matches rendered text, and the ack's prefix IS the rendered tool_result body). Searching `async_launched` does NOT work - that string lives only in `toolUseResult`, which is never rendered, so the search returns only prose mentions.",
"located": {
"claude_code": "2.1.191",
"csift": "0.6.0",
"source": "AGENTS.md section 3.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search 'Async agent launched' --raw --max-count 0 piped to a python3 pass checking the tool_result text prefix and the toolUseResult field set AND a python3 pass over every ~/.claude/projects/*/*.jsonl joining each async ack's tool_use_id to a tool_use in the same file AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'Async agent launched'",
"observed": "906 raw records matched; 297 are tool_results whose text STARTS WITH `Async agent launched successfully`, and on those the toolUseResult carries isAsync 297/297, status 297/297, agentId 297/297, description 297/297, outputFile 297/297, with (isAsync==true AND status=='async_launched' AND agentId present) on 297/297. The other 347 text occurrences are prose mentions, not tool_results. Separate join pass: 272 async_launched acks in main transcripts, and their tool_use_id joins a tool_use named `Agent` on 272/272, 0 other tools. Binary 2.1.258 string (verbatim): `Async agent launched successfully. (This tool result is internal metadata \\u2014 never quote or paste any part of it, including the agentId below, into a user-facing reply.)`",
"rule": "One row per tool_result block; the prefix test is on the joined text of the tool_result content; the field test requires the key to be present in the parsed toolUseResult object.",
"note": "Every element of the claim is confirmed at the measured scale, and the field set is perfectly co-occurring (297/297 on all five keys) rather than merely usually present. The refinement is that 2.1.258 extends the ack sentence with an internal-metadata warning after the matched prefix; csift's ASYNC_LAUNCH_ACK_PREFIX is a `starts_with` test (src/model/classify.rs:89-92) so it is unaffected, but a consumer matching the full sentence would now miss."
}
]
},
{
"id": "SUB-017",
"area": "subagents",
"behavior": "A `SendMessage` whose `input` (or nested `input.message`) carries a `type` other than `message`/`direct` is a control SIGNAL rather than prose. Claude Code 2.1.258 declares nine typed payload kinds in one registry: plan_approval_request, plan_approval_response, shutdown_request, shutdown_approved, shutdown_rejected, task_assignment, task_completed, teammate_terminated, idle_notification. Which side carries which matters: on the SENT side (SendMessage input.type) the corpus shows only `message` (711), `shutdown_response` (41) and `shutdown_request` (38); `shutdown_approved` and `teammate_terminated` are INBOUND `<teammate-message>` payload types, alongside `idle_notification` and `task_assignment`. An absent type would mean a plain message, but `type` was present on 790/790 SendMessage blocks measured.",
"depends": "csift routes those to `agent.communication.signal` instead of `agent.communication.sent`, so a count of sent messages counts real messages and a shutdown handshake stays separately countable.",
"code": [
{
"path": "src/model/peer.rs",
"lines": "266-269",
"snippet": "/// True when a `SendMessage` `input` is a control SIGNAL rather than a prose message (GOLD\n/// §3): the top-level `type` (or a nested `message.type`) is present and is NOT `message`/\n/// `direct` (e.g. `shutdown_request`/`shutdown_response`/…). Absent type ⇒ a plain message.\npub(crate) fn send_message_is_signal(input: Option<&serde_json::Value>) -> bool {"
}
],
"instrument": "`csift search SendMessage <project dir> --raw | jq -c '.message.content[]?|select(.name==\"SendMessage\")|.input|.type // .message.type' | sort | uniq -c` beside `csift search '' <project dir> --count-by label | grep agent.communication`. Counting rule: one row per SendMessage tool_use block.",
"located": {
"claude_code": "2.1.191",
"csift": "0.6.0",
"source": "AGENTS.md section 3.3a"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search 'SendMessage' -t agent.communication --raw --max-count 0 piped to a python3 tally of input.type / input.message.type AND csift search 'teammate-message' --raw --max-count 0 piped to a python3 regex+json parse of each `<teammate-message>` payload's `type` AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'PTr=new Map' parsed for the map's message-type keys",
"observed": "SENT side, 790 SendMessage tool_use blocks corpus-wide: message 711, shutdown_response 41, shutdown_request 38; type absent on 0/790. `shutdown_approved` and `teammate_terminated` never appear as a SendMessage input.type. RECEIVED side, 1177 parsed `<teammate-message>` payloads: <prose, no json payload> 708, idle_notification 334, task_assignment 49, shutdown_request 36, teammate_terminated 25, shutdown_approved 25. Binary 2.1.258 declares a typed-payload registry with NINE keys, in map order: plan_approval_request, plan_approval_response, shutdown_request, shutdown_approved, shutdown_rejected, task_assignment, task_completed, teammate_terminated, idle_notification. The literal \"direct\" also occurs 37x in the binary.",
"rule": "One row per SendMessage tool_use block for the sent side; one row per `<teammate-message>` element for the received side, its type read from the payload's top-level `type` when the body parses as JSON, else counted as prose. Binary keys counted once each from the map literal.",
"note": "csift's rule (anything not message/direct is a signal) classifies all nine binary-declared kinds correctly, so the mechanism holds. Two corrections: (1) the claim's 'observed set' mixes the sent and received directions - shutdown_approved and teammate_terminated were never sent through SendMessage in this corpus, they arrive as inbound teammate-message payloads; (2) the set is larger than four - the binary declares nine, of which four the ledger never listed (plan_approval_request, plan_approval_response, shutdown_rejected, task_completed) and one observed on disk is NOT in the binary registry (`shutdown_response`, 41 sends, though the literal does appear 11x in the binary). Note also `idle_notification` is by far the commonest inbound signal (334 of 469 typed payloads), so a consumer that only models shutdown traffic will misread most teammate signalling."
}
]
},
{
"id": "SUB-018",
"area": "subagents",
"behavior": "A subagent's returned message reaches its parent by one of three carriers, not one: the synchronous tool_result text, the child transcript's tail assistant text when the parent result was the async launch ack, or the workflow journal's `result` event payload; a node that resolves through none of the three carries a null source and no returned message. Measured over 27 sessions / 7678 subagent nodes: workflow-journal 6224, async-child-tail 436, sync-tool-result 396, unresolved 622 - exactly one source per node.",
"depends": "csift implements all three paths and records which one answered as `returned_message_source`; dropping the async-child-tail fallback loses the report for every background-launched agent, and dropping the journal path loses it for the dominant workflow kind.",
"code": [
{
"path": "src/subagent/spawn.rs",
"lines": "264-271",
"snippet": "pub enum ReturnedMsgSource {\n /// A synchronous built-in: the parent tool_result text IS the returned message.\n SyncToolResult,\n /// An async built-in (`Async agent launched …` sentinel): the message is the child\n /// transcript's tail assistant text.\n AsyncChildTail,\n /// A workflow agent: the message is the `journal.jsonl` `result` event payload.\n WorkflowJournal,"
}
],
"instrument": "`csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | .returned_message_source' | sort | uniq -c`. Counting rule: one node per subagent transcript, exactly one source per node.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.7",
"source": "SPEC.md section 10.3; SPEC.md section 6 v0.6.7 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift agents @<session-uuid> --returned-message --format json | jq -r 'select(.kind==\"agent\").returned_message_source' | sort | uniq -c run first on one 579-node session and then looped over every session dir holding subagent transcripts",
"observed": "One 579-node session: workflow-journal 402, async-child-tail 91, sync-tool-result 53, null 33 (402+91+53+33 = 579 exactly, so exactly one source per node and no node carries two). Looped over 27 sessions / 7678 agent nodes: workflow-journal 6224 (81.1%), null 622 (8.1%), async-child-tail 436 (5.7%), sync-tool-result 396 (5.2%). Nodes with a null source are exactly the nodes with no returned_message (33/33 on the single session).",
"rule": "One node per subagent transcript; source read from the node's `returned_message_source`; the four buckets must sum to the node count, which is the check that a node never carries two sources.",
"note": "All three carriers are exercised, and the structural invariant the claim rests on (exactly one source per node) is verified by the bucket sum on a 579-node session. The claim's specific counts (147 / 6 / 403 / 1) do not reproduce anywhere and cannot: they are a snapshot of one unnamed session at csift 0.6.7, and transcripts grow. Replaced with a corpus-scale distribution carrying its counting rule. The 'unresolved' share is materially larger than the claim implies - 8.1% corpus-wide, not ~0.2% - so a consumer must handle a null source as a normal outcome rather than an anomaly. The 'zero linkage mismatches' assertion has no instrument behind it here: nothing in the output distinguishes a correct join from a wrong one, so it stays an assertion."
}
]
},
{
"id": "SUB-019",
"area": "subagents",
"behavior": "The parent's record of a subagent return is not the child's own conclusion, but not because of truncation: Claude Code 2.1.258 APPENDS a continuation footer `agentId: <id> (use SendMessage with to: '<id>', summary: '<5-10 word recap>' to continue this agent)` to the subagent tool_result (present on 813 of 919 tool_results carrying `agentId: `). When the child's last assistant message is a terse sign-off - `Done.`, `Complete.`, `Standing by.` - the parent's return reads as a bare wrapper, but the terse text is the child's own final words, not a harness truncation of a longer answer; the substantive report sits in an earlier message of the same child transcript. The operational rule is unchanged: read the child's own words with `csift show @<agent-id> --turn -1..`.",
"depends": "csift tags every return with its resolution source (returned_message_source: sync-tool-result, async-child-tail or workflow-journal) and points at the child's own final words (show @<agent-id> --turn -1..); a sync return is the ORCHESTRATOR's record - the child's own terse sign-off plus the harness's appended continuation footer - so reading returned_message as the agent's conclusion misreads a sign-off, not a truncation.",
"code": [
{
"path": "src/subagent/spawn.rs",
"lines": "254-256",
"snippet": "/// The synthesized prefix Claude Code writes into a tool_result when a subagent is\n/// launched ASYNCHRONOUSLY (run_in_background) - the real returned message is then NOT in\n/// the parent tool_result but in the child transcript tail."
},
{
"path": "src/cli/agents_args.rs",
"lines": "123",
"snippet": "\\\"what did the agent conclude\\\": a `sync-tool-result` source faithfully reports \\"
},
{
"path": "src/subagent/spawn.rs",
"lines": "260-266",
"snippet": "/// Where a subagent's returned message was resolved FROM (§3) - surfaced so a consumer\n/// knows whether it read the parent tool_result, the child transcript tail, or the\n/// workflow journal.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ReturnedMsgSource {\n /// A synchronous built-in: the parent tool_result text IS the returned message.\n SyncToolResult,"
},
{
"path": "src/cli/agents_args.rs",
"lines": "122-128",
"snippet": " SEMANTICS: it answers \\\"what did the ORCHESTRATOR record as the return\\\", not \\\n \\\"what did the agent conclude\\\": a `sync-tool-result` source faithfully reports \\\n the parent's tool_result even when it is a terse sign-off (`Done.`, `Complete.`) \\\n plus the harness's appended continuation footer (`agentId: <id> (use SendMessage \\\n with to: '<id>', summary: '<5-10 word recap>' to continue this agent)` - the harness \\\n appends, it never truncates); the child's own final words are always \\\n `csift show @<agent-id> --turn -1..`). \\"
}
],
"instrument": "`csift agents @<session> --returned-message --format json | jq -r 'select(.returned_message_source==\"sync-tool-result\") | .returned_message' | rg '^Done\\. agentId'`, then compare each hit with `csift show @<agent-id> --turn -1`. Counting rule: wrapper-shaped returns over sync returns.",
"located": {
"claude_code": "2.1.258",
"csift": "0.6.7",
"source": "SPEC.md section 6 v0.6.7 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "csift search 'Done\\. agentId' --raw --max-count 0 piped to a python3 pass counting tool_results whose text starts with `Done. agentId:` AND csift search 'agentId: ' --raw --max-count 0 piped to a python3 pass splitting each tool_result at the harness footer regex `agentId: [0-9a-f]{8,}[^\\n]*(use SendMessage with to:|do not mention to user)` AND csift show @<agent-id> --turn -1 on six terse-return children AND strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -cN 'Done\\. agentId'",
"observed": "Tool_results whose text starts with `Done. agentId:`: 0 corpus-wide. Binary occurrences of `Done. agentId`: 0 (the only `Done.` strings in 2.1.258 are prompt instructions telling agents NOT to emit confirmations like \"Done.\"). What the corpus actually holds: 919 tool_results containing `agentId: `, of which 813 carry the harness footer, whose 2.1.258 template is verbatim `agentId: ${e.agentId} (use SendMessage with to: '${e.agentId}', summary: '<5-10 word recap>' to continue this agent)`. Splitting at that footer: 641 have a long head, 172 a head under 40 chars - the short heads are `Async agent launched successfully.` 103, `Complete.` 15, `Done.` 14, `Complete. No further action.` 4, `DONE` 3, `Standing by.` 3, and a long tail of similar sign-offs. Fetching six of those children's own last turn showed the child's final assistant message IS the terse text (e.g. a child whose parent return read `Done.` ends L16 `Done.` L17 `Done.`; another ends `Complete.`), with the substantive report several messages EARLIER in the same tail.",
"rule": "One row per tool_result block. A record counts as wrapper-shaped only if its joined tool_result text starts with the literal `Done. agentId:`. For the footer split, the head is everything before the first footer-regex match.",
"note": "The mechanism named in the claim is not observable in Claude Code 2.1.258: there is no truncation-to-wrapper. The harness appends a continuation footer and, separately, splits result content into harness notes / body / tail sections verified by a content hash (the 2.1.258 helper takes a note count, a tail count and a section hash and, on hash mismatch, returns the body untouched rather than trimming it). Zero tool_results in the corpus start with `Done. agentId:` and the string does not exist in the binary. The consequence csift documents is still true and still worth documenting - the orchestrator's record of a return is often uninformative - so the fix is a wording correction at three doc sites (one of which is `--help`, hence a versioned surface change), not a removal."
}
]
},
{
"id": "SUB-020",
"area": "subagents",
"behavior": "A teammate is an in-process Agent subagent that holds no background-task id: Claude Code 2.1.258 resolves `TaskStop` against its task registry and answers `No task found with ID: <id>` on a miss, and the on-disk registry under ~/.claude/tasks holds 0 entries of the teammate id shape among 517 entries - so every id form csift can print (the name, the `Name@team` form, the `a<Name>-<hash>` agent id) misses. It also has no separate OS process (spawn acks carry is_splitpane false with the orchestrator's own tmux pane id). The working control path is `SendMessage` addressed by name; 2.1.258 declares `shutdown_request` as a typed payload kind alongside `shutdown_approved`, `shutdown_rejected` and `teammate_terminated`, and the corpus records the full handshake completing.",
"depends": "csift is read-only, so naming the correct control tool is the whole remedy: `agents` prints a fact-led two-line hint as a text footer whenever a teammate is in scope and the same pointer as each teammate node's JSON `control_hint`. Without it a CORRECT teammate id reads as a wrong id when the wrong tool rejects it - a real session spent about 30 minutes on that mistake.",
"code": [
{
"path": "src/agents/run.rs",
"lines": "230-234",
"snippet": "pub(crate) const TEAMMATE_CONTROL_HINT_L1: &str = \"note: teammate rows are in-process Agent subagents — address one BY NAME (the `(@name)` shown) \\\nvia SendMessage to steer it, and `message:{\\\"type\\\":\\\"shutdown_request\\\"}` to terminate it.\";\npub(crate) const TEAMMATE_CONTROL_HINT_L2: &str =\n \" A teammate is NOT a background task (TaskStop / a `task_id` will not find it) and has no \\\nseparate OS process (it shares the orchestrator PID — `pkill` won't help).\";"
}
],
"instrument": "In a live session holding a teammate, call TaskStop with the exact agent id `csift agents` printed and read the rejection text, then send SendMessage `{type:\"shutdown_request\"}` addressed to the teammate name and observe the lane stop; `csift agents @<session> --shape teammate --format json | jq -r .control_hint` must carry the same pointer. Counting rule: one attempt per control tool. The tool contract itself is re-verifiable only against a live SendMessage schema, not from a static corpus.",
"located": {
"claude_code": "2.1.191",
"csift": "0.3.0",
"source": "AGENTS.md section 3.7 control hint; SPEC.md section 6.5; SKILL.md Hook 2"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'No task found with ID' AND strings -n 6 ... | rg -N 'PTr=new Map' (the SendMessage typed-payload registry) AND find ~/.claude/tasks -maxdepth 2 -mindepth 2 | sed 's|.*/||;s|\\.json$||' | rg -c '^a[A-Za-z0-9-]+-[0-9a-f]{16}$' AND csift search 'No task found with ID' --max-count 0 AND a python3 pass over the teammate-owning transcripts pairing SendMessage input.type==shutdown_request with inbound `<teammate-message>` payload types AND csift agents @<session-uuid> --shape teammate (text footer + JSON control_hint)",
"observed": "Binary 2.1.258 generates the rejection from a task-registry lookup: `function SOn(e,n){return \\`No task found with ID: ${e}${h$e(n.taskRegistry,n.getAppState,xde(n))}\\`}`. On-disk task registry: 517 entries under ~/.claude/tasks, of which 0 match the teammate id shape `^a[A-Za-z0-9-]+-[0-9a-f]{16}$` (ids there are small integers under per-session dirs). Corpus holds the actual recorded rejections as tool_result text - `<tool_use_error>No task found with ID: <teammate name></tool_use_error>`, the same for the `<name>@session-<id>` form, and the same for the exact `a<Name>-<16 hex>` agent id - three id forms, all rejected. Teammate spawn acks: is_splitpane false on 156/156 with a non-empty tmux_pane_id (the orchestrator's own pane). Shutdown path: SendMessage input.type==shutdown_request sent 38 times; 2 of 4 teammate-bearing transcript groups show a shutdown_request sent AND a shutdown_approved / teammate_terminated coming back; the binary declares shutdown_request, shutdown_approved, shutdown_rejected and teammate_terminated as typed payload kinds. csift emits the two-line hint as a text footer and control_hint on 49/49 teammate rows.",
"rule": "One attempt per control tool per id form for the rejection side; one row per registry entry file for the task-registry check; one row per SendMessage tool_use block and one per `<teammate-message>` payload for the handshake pairing.",
"note": "The rejection text itself is a historical recorded observation (the three rejected id forms are preserved verbatim in a transcript tool_result from an earlier Claude Code), not a fresh live call at 2.1.258. It is re-confirmed indirectly at 2.1.258 in two independent ways: the rejection is emitted from a taskRegistry lookup in the current binary, and the current on-disk task registry contains no teammate-shaped id at all (0 of 517). What would settle it directly: calling TaskStop with a running teammate's exact agent id in a live 2.1.258 session and reading the error, which needs a live teammate this verification pass did not hold. The SendMessage half is better evidenced than the claim states - both the typed-payload registry in the binary and two transcripts recording a sent shutdown_request answered by shutdown_approved / teammate_terminated."
}
]
},
{
"id": "SUB-021",
"area": "subagents",
"behavior": "Claude Code exports `CLAUDE_CODE_SESSION_ID` into the Bash tool environment and its value equals the session's own jsonl basename exactly - but it names the TOP-LEVEL session in EVERY lane: a subagent (an Agent-tool subagent and a workflow agent alike) is handed the PARENT session uuid and its own id is withheld from the Bash env, given only to hooks. Older builds handed a built-in Task subagent its own id.",
"depends": "csift's `whoami` uses that variable and nothing else - never a process-tree walk, never most-recent-mtime, which is a false-positive trap under concurrent sessions - refuses to guess when it is absent, and reports `is_subagent`/`parent_session_id`/`depth` as NULL with a stderr lane note on the env form rather than fabricating false/0. `@trap:<marker>` exists as the env-independent self-resolution path precisely because a running subagent cannot name itself from the env.",
"code": [
{
"path": "src/whoami.rs",
"lines": "3-7",
"snippet": "//! ## Detection (verified empirically inside a live Claude Code Bash tool, 2026-06-07)\n//!\n//! Claude Code exports `CLAUDE_CODE_SESSION_ID` into its Bash tool environment.\n//! It was confirmed to equal exactly the session's own jsonl filename:\n//!"
},
{
"path": "src/whoami.rs",
"lines": "33",
"snippet": "const SESSION_ID_ENV: &str = \"CLAUDE_CODE_SESSION_ID\";"
},
{
"path": "src/whoami.rs",
"lines": "98-100",
"snippet": " // C-16 lane honesty: the env names the TOP-LEVEL session in EVERY lane (current CC\n // hands a subagent its parent's id), so env-only resolution cannot know whether the\n // CALLER is that session. Say so on stderr; the unknowable JSON fields are null."
}
],
"instrument": "In a top-level Bash call, `echo $CLAUDE_CODE_SESSION_ID` then `ls ~/.claude/projects/*/$CLAUDE_CODE_SESSION_ID.jsonl` must match exactly one file; from inside a subagent, the same echo prints the PARENT uuid while `csift whoami @trap:<a fresh three-CamelCase-word plus four-digit marker>` prints the subagent's own ancestry chain, and `csift whoami --format json` reports the lane fields null. Counting rule: one env value per lane, one string comparison of the two ids. Requires a live subagent lane; it cannot be reproduced from a static corpus.",
"located": {
"claude_code": "2.1.150",
"csift": "0.1.0",
"source": "AGENTS.md section 3.8; SPEC.md sections 6.3 and 6.3a; CHANGELOG 0.8.2 (lane honesty); src/whoami.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "Run from inside a LIVE workflow-subagent lane: (1) `echo \"LEN=${#CLAUDE_CODE_SESSION_ID}\"; ls ~/.claude/projects/*/\"$CLAUDE_CODE_SESSION_ID\".jsonl | wc -l`; (2) `csift whoami @trap:<fresh three-CamelCase-word + four-digit marker> --format json`; (3) python string comparison of the env value against the trap-resolved own agent id and against the trap-resolved parent_session_id; (4) `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'CLAUDE_CODE_SESSION_ID'` plus a raw-byte context dump around each of its 13 occurrences.",
"observed": "env value length 36, uuid-shaped, matched exactly 1 file under ~/.claude/projects/*/<value>.jsonl. `whoami @trap:` emitted 2 identity rows: the caller's own transcript (a .../subagents/workflows/wf_*/agent-*.jsonl path, is_subagent true, a 17-char bare-hex agent id) and its top-level ancestor. String comparison: env == the resolved parent_session_id -> True; env == the caller's own agent id -> False. Binary (2.1.258), child-env builder: `function OOe(e){let n={CLAUDECODE:\"1\",CLAUDE_CODE_SESSION_ID:e.sessionId,CLAUDE_CODE_CHILD_SESSION:\"1\",CLAUDE_PID:String(process.pid)};if(e.source==\"agent\")n.AI_AGENT=...}` and the shell call sites pass `OOe({sessionId:Q(),effortLevel:z,source:\"agent\"})`. Binary hook-input schema: `agent_id:i().optional().describe(\"Subagent identifier. Present only when the hook fires from within a subagent (e.g., a tool called by an AgentTool worker). Absent for the main thread, even in --agent sessions. Use this field (not agent_type) to distinguish subagent calls from main-thread calls.\")`. Live shell env also carries CLAUDE_CODE_CHILD_SESSION=1 and CLAUDE_PID alongside the session id, exactly as that builder writes them.",
"rule": "One env value read per lane; one glob count of top-level transcripts whose basename equals it (expect exactly 1); two string equality tests (env vs own id, env vs parent id) against the ids the env-independent @trap path resolved in the same lane.",
"note": "Every code site is verbatim in the current tree: src/whoami.rs:3-7 (module doc), :33 (`const SESSION_ID_ENV: &str = \"CLAUDE_CODE_SESSION_ID\";`), :98-100 (the C-16 lane-honesty comment, confirmed by `grep -n \"C-16 lane honesty\" src/whoami.rs` -> 98). One clause of the claim is not decidable on this machine: 'older builds handed a built-in Task subagent its own id' is a statement about superseded Claude Code versions - deciding it would need a live Bash call inside a built-in Task subagent under one of those older binaries. Everything about 2.1.258 checked out: the id a subagent is handed is the top-level parent's, and the subagent's own id reaches only hooks (documented `agent_id` field), which is why an env-independent self-resolution path is needed at all."
}
]
},
{
"id": "SUB-022",
"area": "subagents",
"behavior": "A SUBAGENT transcript flushes per content block as the block closes, so its launching tool_use record is on disk while that very command runs (verified: a first-use @trap resolves, exit 0). The MAIN conversation instead writes an async flush of the completed assistant message - every block of the message lands at one wall-clock instant - and the tool_use record was measured landing 0.10-2.80 s (n=17, p50 0.57 s) after its own timestamp, not the earlier-stated 1-3.4 s band. It is a race, not a wait: csift's own trap scan finishes in 0.05-0.06 s, well inside the window, so a main-lane first use still normally misses.",
"depends": "csift's `@trap:<marker>` self-resolution therefore hits on the FIRST try from a subagent lane but normally misses on a top-level first use and resolves on a re-run of the SAME marker (a second attempt inside ONE script runs in that same unlanded window); the no-match error routes `@main` first, then the literal-marker check, then retry-as-lane-confirmation. The same window makes a UserPromptSubmit hook asking for the PREVIOUS prompt exclude hits younger than now minus three seconds and take the newest survivor.",
"code": [
{
"path": "src/path/trap.rs",
"lines": "12-19",
"snippet": "/// Resolve `@trap:<marker>` to the CALLING agent/session by finding the transcript whose Bash\n/// `tool_use` command carries the (unique, literal) marker AND the literal `csift` - i.e. the\n/// very command that launched this run. Mechanism + TIMING (subagent verified live 2026-07-12;\n/// main-lane mechanism re-measured 2026-08-29): a SUBAGENT's transcript flushes per content\n/// block as it closes, so its launching tool_use is on disk at dispatch and a first try\n/// resolves. The MAIN conversation writes an async flush of the COMPLETED assistant message\n/// that lands ~1-3.4s AFTER the tool was dispatched - a RACE, not a wait (a 263s command was\n/// observed with its unpaired tool_use already on disk 39s in). csift finishes well inside"
},
{
"path": "src/path/trap.rs",
"lines": "302-308",
"snippet": "pub(crate) fn bash_command_carries_trap(path: &Path, marker: &str) -> bool {\n let Ok(Some(mmap)) = crate::parse::mmap_bytes(path) else {\n return false;\n };\n let bytes: &[u8] = &mmap;\n if memchr::memmem::find(bytes, marker.as_bytes()).is_none() {\n return false;"
}
],
"instrument": "From a main-thread Bash call run `csift whoami @trap:<a fresh three-CamelCase-word plus four-digit marker>` and then re-run the IDENTICAL command as a SECOND, SEPARATE invocation: expect a no-match error followed by a resolution; from inside a subagent the first try must resolve (exit 0). Counting rule: one attempt per separate shell-tool invocation, exit code recorded; a second attempt inside one script does not count. Measure the window as the difference between the tool_use record's `timestamp` and the wall-clock dispatch time.",
"located": {
"claude_code": "2.1.191",
"csift": "0.4.0",
"source": "AGENTS.md section 2 TIMING; SPEC.md section 6.3a; CHANGELOG 0.8.2 (@trap timing corrected); src/path/trap.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) Subagent half: `csift whoami @trap:<fresh marker> --format json; echo EXIT=$?` as the FIRST use of that marker, from inside a live workflow-subagent lane. (b) Main-lane half: an inline python poller against the parent's main transcript - stat the file every 0.15 s, read only the newly appended bytes, and for each new record print (wall-clock time the bytes were first seen) minus (the record's own `timestamp` field); ran 240 s. (c) `/usr/bin/time -p csift whoami @trap:<marker>` x3 to size csift's own runtime against that window. (d) `csift agents @<session> --format json` filtered to rows carrying pending_tool_use_id.",
"observed": "(a) EXIT=0 on the first invocation - the launching Bash tool_use was already on disk while its own command was still running (a later 3-run timing loop with a second fresh marker also resolved every time). (b) 240 s of main-lane appends: assistant records carrying a tool_use block n=17, landing lag min 0.10 s, p50 0.57 s, max 2.80 s; assistant thinking-block records n=18, lag min 0.17 s, p50 3.01 s, max 18.67 s; user tool_result records n=17, min 0.12 / p50 0.51 / max 1.29; the blocks of one assistant message appear at the SAME wall-clock instant, so the earliest block of a message carries the largest lag - the async-flush-of-the-completed-message mechanism. (c) csift trap resolution wall time 0.05, 0.05, 0.06 s. (d) 3 lanes were live with an unreturned tool_use at that instant.",
"rule": "One @trap attempt per SEPARATE shell-tool invocation, exit code recorded. Flush lag = (wall clock when the appended bytes were first observed, 0.15 s poll resolution) - (that record's own ISO `timestamp`); one measurement per newly appended record, bucketed by record type and block type.",
"note": "Code sites verbatim: src/path/trap.rs:12-19 (the TIMING doc comment, `grep -n` -> 12 and 19) and src/path/trap.rs:302-308 (`pub(crate) fn bash_command_carries_trap`, at 302). The subagent half is directly instrumented and holds. The main-lane half was measured from the receiving end (record appearance vs the record's own timestamp) rather than from a main-thread dispatch clock, because a subagent cannot issue a main-thread shell call; that measurement puts the floor below the claimed 1 s and the ceiling below the claimed 3.4 s, but leaves the mechanism and the race conclusion intact. The main-lane 'first try misses, a re-run of the same marker resolves' pair itself was not executed here - it needs two separate shell-tool invocations from the top-level lane."
}
]
},
{
"id": "SUB-023",
"area": "subagents",
"behavior": "A subagent's own first-record timestamp lags the parent Task/Agent/Workflow tool_use timestamp by a median of 0.20 s (n=772 nodes over 20 sessions), with half the population below 0.2 s, a p90 of 0.73 s, a p99 of 3.35 s and a long tail out to 104.7 s - too coarse, and too heavy-tailed, to order the siblings of a parallel fan-out.",
"depends": "csift makes the parent tool_use instant the default `--order-by trigger` axis and keeps the child's `started_utc` as a secondary timestamp; ordering by start would scramble a parallel fan-out.",
"code": [
{
"path": "src/subagent/types.rs",
"lines": "49-50",
"snippet": " /// built-in `meta.json` `toolUseId` (on disk for every built-in subagent; `None` for\n /// workflow agents, whose meta carries only `agentType`). This is the join key into"
}
],
"instrument": "`csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | [.trigger_utc, .started_utc] | @tsv'` and difference each pair. Counting rule: one delta per agent node carrying both instants.",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "SPEC.md section 6.5"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "For every session root on the machine that has a subagents/ directory (25 found by `ls ~/.claude/projects/*/*/subagents`), run `csift agents @<session> --format json`, keep `kind==\"agent\"` rows that carry a non-null spawn_tool_use_id AND both instants, drop rows where trigger_utc == started_utc verbatim (those are the documented fallback where trigger could not be joined and is copied from the child head), and difference each remaining pair. Separately, chain those rows into fan-out batches (consecutive spawns <= 5.0 s apart, size >= 2) and count sibling pairs whose start order inverts their trigger order.",
"observed": "20 sessions contributed 772 agent rows with a real parent-tool_use join and distinct instants (plus 32 rows dropped as verbatim-equal fallbacks). Lag seconds: min 0.071, p10 0.165, p50 0.200, p90 0.733, p99 3.352, max 104.685. 384 of 772 rows (50%) are BELOW 0.2 s; only 3 rows exceed 4.7 s. By shape: builtin-task n=627 min 0.071 p50 0.195 max 104.685; teammate n=145 min 0.170 p50 0.227 max 4.002. Fan-out check: 15 batches, 17 sibling pairs, 1 start-order inversion (5.9%).",
"rule": "One delta per agent node that carries BOTH a real spawn join (spawn_tool_use_id non-null) and two distinct instants; verbatim-equal pairs excluded as fallback artifacts, not as zero-lag measurements. Inversion = a sibling pair (a,b) in one batch with trigger_a < trigger_b but started_a > started_b.",
"note": "The stated 0.2-4.7 s range turns out to be roughly the p50-to-p99 band, not the range: the measured floor is 0.071 s and the measured ceiling 104.685 s (one builtin-task node; the next largest are 4.87 and 4.71 s). The design consequence is unchanged and is now directly supported rather than inferred - 1 of 17 sibling pairs in a fan-out chain starts out of trigger order, so ordering by start would scramble a fan-out. Code site verbatim: src/subagent/types.rs:49-50 (the spawn_tool_use_id doc comment, `grep -n` -> 49)."
}
]
},
{
"id": "SUB-024",
"area": "subagents",
"behavior": "A permission escalation still leaves NO jsonl trace and no control-file trace: a tool_use block carries only {type,id,name,input,caller} (5753/5753 blocks), no permission-field name occurs in any transcript of the sampled project dir, and the session registry records only busy/idle/shell. On disk the blocked lane is an assistant tool_use record standing as the LAST record with no tool_result for its id - but `stop_reason:\"tool_use\"` is only reliably part of that signature in a MAIN transcript (5768/5768 there); in a subagent transcript the per-block flush writes the record before the message closes, so 1578 of 3352 subagent tool_use records carry `stop_reason:null`. csift's frozen verdict keys on the record shape, not on stop_reason, so it is unaffected.",
"depends": "csift's `lifecycle` reads the frozen verdict from the NEWEST meaningful record (the first non-metadata record from EOF) and forces status Running, because the older tail walk-back found the assistant text PRECEDING the frozen call and reported a blocked lane as completed; it then classifies escalation-blocked only when the ported dangerous-command classifier would hoist that command, and otherwise says awaiting-execution rather than pretending to tell slow from wedged.",
"code": [
{
"path": "src/subagent/lifecycle.rs",
"lines": "35-40",
"snippet": " // TAIL: last record's timestamp == completion (best-effort), whether the transcript\n // terminates with a visible assistant message (a clean finish), AND whether the lane is\n // FROZEN at an unreturned tool_use. The frozen verdict comes from the NEWEST meaningful\n // record only (the first non-metadata record from EOF): if it is an assistant tool_use, no\n // tool_result followed it (it IS the last record) ⇒ the lane is blocked there, NOT done. The\n // terminal_agent_msg walk-back is UNCHANGED for every non-frozen lane."
}
],
"instrument": "Trigger a permission prompt and, while it waits, run `csift agents @<session> --format json | jq 'select(.pending_tool_use_id) | {pending_classification, pending_since_utc, status}'` - the row must be pending and never `completed`; confirm the transcript's final line is a `tool_use` whose id has no later `tool_result`. Counting rule: the last record's shape at that instant, one pending row per frozen lane.",
"located": {
"claude_code": "2.1.220",
"csift": "0.6.x",
"source": "AGENTS.md section 3.9; src/subagent/lifecycle.rs comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(1) Key census: parse every `tool_use` block in the 4 largest top-level transcripts of one project dir and print the union of block keys with counts. (2) `rg -c 'permissionPromptStartTimeMs|\"isEscalated\"|\"requires_approval\"|\"permission_request\"|\"pendingPermission\"' *.jsonl` across the 23 top-level transcripts of that project dir. (3) Registry: `python -c` over ~/.claude/sessions/*.json printing the observed `status` and `kind` values and the key set of one file. (4) Live frozen-lane specimen: read the last line of a currently-executing lane's transcript, check the block types and whether the tool_use id appears on any later line; and `csift agents @<session> --format json` filtered to rows with pending_tool_use_id. (5) `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'permission[A-Za-z_]{0,24}'` plus raw-byte context around `hook_event_name`.",
"observed": "(1) 5753 tool_use blocks; key union EXACTLY {type:5753, id:5753, name:5753, input:5753, caller:5753} - no permission, isEscalated or requires_approval key on any block. (2) zero matching lines across 23 top-level transcripts. (3) 7 live registry files, keys [bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version]; status values observed {busy:2, idle:4, shell:1}, kind {interactive:7} - no permission-pending state. (4) The live lane's last record: type=assistant, blocks=['tool_use'], its tool_use id appears on exactly 1 line of the whole file (no tool_result), block keys ['caller','id','input','name','type']; `csift agents` reported 3 rows with status=running, pending_tool_name=Bash, pending_classification=awaiting-execution, completed_utc=None. (5) The binary carries a first-class hook event: `hook_event_name:x(\"PermissionRequest\"),tool_name:i(),tool_input:de(),permission_suggestions:R(_i()).optional()`, and permission requests travel the in-process messaging inbox (`[InboxPoller] Dropping permission request `, `: agent_id `, ` does not match sender `), never a transcript record.",
"rule": "One key-union census over every tool_use block in the 4 largest top-level transcripts of one project dir; one grep count of permission-field names over that dir's 23 top-level transcripts (expect 0); the last record's shape at one instant, one pending row per frozen lane.",
"note": "Code site verbatim: src/subagent/lifecycle.rs:35-40 (the TAIL comment, `grep -n` -> 35). What an instrument here could and could not decide: the absence of a trace and the shared signature were measured, and the in-flight branch of the signature was captured live (3 lanes frozen at an unreturned Bash tool_use, reported running/awaiting-execution and never completed). The escalation-blocked branch itself was NOT produced - triggering a real permission prompt from this lane would block the session on a human, so 'the same signature also covers an escalation-blocked lane' rests on the absence of any distinguishing field rather than on a captured specimen. One thing has moved since the claim was written: 2.1.258 ships a dedicated `PermissionRequest` hook event, so the higher-fidelity live signal the claim describes as needing a Notification or PreToolUse hook now has a purpose-built event to hang a sidecar on."
}
]
},
{
"id": "SUB-025",
"area": "subagents",
"behavior": "Background-shell and subagent completion notifications land in a session's MAIN transcript only: across 8 session roots holding 372 subagent transcripts, all 180 classified `<task-notification>` carriers sit in the main file and 0 sit in any subagent transcript.",
"depends": "csift scopes `wait --until notification` to the main lane on that basis and reads background carriers only from the main file; a subagent-lane scan for shell completions would find nothing to join.",
"code": [
{
"path": "src/live/wait.rs",
"lines": "157-161",
"snippet": " for (raw, cond) in &conds {\n if record_matches(cond, &rec, cur.is_main) {\n return finish(args, raw, &main, &lens, &activity, start.elapsed());\n }\n }"
}
],
"instrument": "`rg -c '<task-notification>'` is not a sound counting rule for this claim - in this corpus 661 subagent lines contain the literal tag as prose (relayed instructions and documentation quoting it) while 0 are carriers. Count carriers with `csift search '' -t harness.notification <target> --max-count 0`, or by requiring the record's message text to START with the tag.",
"located": {
"claude_code": "2.1.237",
"csift": "0.9.0",
"source": "dev session 2026-08-30"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "Per session root in one project dir: `csift search '' -t harness.notification @<session> --max-count 0 --format json`, counting hits by their own is_subagent flag and by label. Cross-check of the claim's own instrument: `rg -c '<task-notification>' <main>.jsonl` beside a recursive count over that session's subagents/*.jsonl, then a python pass over every subagent transcript in the project dir classifying each matching line as carrier (its message text STARTS with the tag) or prose. Corpus-wide caveat probe: `csift search '' -t harness.notification.monitor --max-count 0 --format json`, and `rg -c --glob '*/subagents/**/*.jsonl' '\"name\":\"Monitor\"'` under ~/.claude/projects.",
"observed": "8 session roots with subagents (372 subagent transcripts between them, largest 255): classified notification carriers main=3/4/108/0/1/0/42/22, subagent=0 in every one - 180 main-lane carriers, 0 subagent-lane carriers. Labels present: harness.notification.background-command 128, .subagent 46, .workflow 2. The raw literal count over the same subagent transcripts was 661 lines, of which 0 are carriers (all prose mentions of the tag). Corpus-wide monitor notifications: 946 records over 4 sessions, main=946 subagent=0; and `\"name\":\"Monitor\"` appears on 0 lines of any subagent transcript on this machine.",
"rule": "One carrier per record whose message text STARTS with `<task-notification>` (equivalently: one csift hit under `-t harness.notification`), bucketed by the hit's own is_subagent flag. Lines that merely contain the literal string are NOT carriers and must not be counted.",
"note": "Code site verbatim: src/live/wait.rs:154-158 (the condition loop; note the same 5 lines also appear at 166-170, so a stranger anchoring by text alone will find two hits). Two numeric drifts in the claim's specimen sentence: one specimen still reads exactly 3 carriers over 9 subagent transcripts, but the 42-carrier specimen now has 255 subagent transcripts rather than 54 (the session kept running). The stated caveat - 'a Monitor armed by a subagent notifies into that subagent's own lane' - is untested here rather than confirmed: all 946 monitor notifications on this machine landed in main lanes, and no subagent transcript in the corpus contains a Monitor tool_use at all, so no subagent ever armed one. Deciding that caveat needs a session where a subagent itself arms a Monitor and the resulting pulse is located."
}
]
},
{
"id": "SUB-026",
"area": "subagents",
"behavior": "A WORKFLOW-agent transcript (`<uuid>/subagents/workflows/wf_<id>/agent-<hex>.jsonl`) never persists the structured `toolUseResult` echo for a LANDED file tool: the tool_result block carries only the bare model-facing string (`File created successfully at: <path>` / `The file <path> has been updated`), so the `{type, filePath, content, structuredPatch, originalFile}` object a top-level session writes does not exist in that lane at all.",
"depends": "csift `recover` reconstructs content from that echo, so it carries an input-side fallback rebuilding the mutation from the tool_use INPUT (`Write.content`, `Edit.old_string`/`new_string`, `MultiEdit.edits[]`) gated on the ABSENCE of a carrier (`ids_with_result`); without it every file written by a workflow agent reports `no recoverable history` even though `files` and `search` see the write, because those read the tool_use input directly.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "35-41",
"snippet": "/// WHY: a subagent (built-in Task/Agent-tool) and a workflow-agent transcript record the\n/// tool RESULT as a bare `tool_result` string (`\"File created successfully at: …\"`) with\n/// NO structured `toolUseResult` echo - unlike a top-level session, whose carrier carries\n/// `{type:create, filePath, content, …}`. `extract_from_tool_use_result` reads that echo,\n/// so without this fallback a file WRITTEN BY A SUBAGENT is invisible to `recover`\n/// (`no recoverable history`) even though `files`/`search` see it (they read the tool_use\n/// input directly). The authoritative content IS in the input - `Write.content`,"
},
{
"path": "src/recover/scan.rs",
"lines": "400-406",
"snippet": " // tool_use_ids whose result carrier carries the structured `toolUseResult` echo -\n // i.e. the ops `extract_from_tool_use_result` can reconstruct from. SUBAGENT and\n // workflow-agent transcripts OMIT `toolUseResult` (the tool_result is just a\n // `\"File created successfully…\"` string), so those ids are absent here and the\n // input-side fallback below supplies their content (§ subagent recover).\n let mut ids_with_result: std::collections::HashSet<String> =\n std::collections::HashSet::new();"
}
],
"instrument": "python3 walk over ~/.claude/projects (journal.jsonl excluded), lane by path component, one count per tool_result BLOCK bucketed by whether the carrying record's toolUseResult is a dict. Observed 2026-09-02 at Claude Code 2.1.258: workflow lanes 1788 create results / 0 with the echo and 2734 update results / 0 with the echo, and 2594 of 2594 non-null workflow toolUseResult values string-typed (zero dict-valued). The earlier '1746' create count and the '600 most-recently-modified transcripts' second pass are superseded; only the zero is stable.",
"located": {
"claude_code": "2.1.177",
"csift": "0.2.0",
"source": "src/recover/events.rs:32-46 doc comment; dev session 2026-08-23; proportions measured 2026-09-02"
},
"first_seen_claude_code": "2.1.156",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY' # os.walk ~/.claude/projects, skip journal.jsonl; lane from path components (has 'workflows' => workflow, has 'subagents' only => built-in, neither => top level); parse a line only when it holds the marker bytes; for every tool_result BLOCK whose rendered text holds the marker, tally (lane, isinstance(rec['toolUseResult'], dict)) PY ; python3 - <<'PY' # os.walk ~/.claude/projects restricted to paths containing a 'workflows' component, skip journal.jsonl; for every record holding b'\"tool_result\"' tally (block['is_error'] is True, json type of block['content']) per BLOCK and (record has an errored tool_result, record['toolUseResult'] is not None) per RECORD PY",
"observed": "Workflow lanes: 1788 tool_result blocks whose text holds 'File created successfully at:', 0 of them on a record with a dict-valued toolUseResult; 2734 blocks whose text starts 'The file ' and holds ' has been updated', again 0 with a dict-valued toolUseResult. Stronger form from the workflow-only pass: across every workflow lane the toolUseResult field is present on exactly 2594 records and is string-typed on 2594 of 2594, so a dict-valued toolUseResult does not occur in that lane at all. Same pass, top level for contrast: 1824 create blocks, 1824 with the dict echo.",
"rule": "One count per tool_result BLOCK whose rendered text holds the marker (a string content is the text; an array content contributes its joined text blocks). Lane is decided by path components only. journal.jsonl excluded. The corpus is live and append-only, so the absolute counts grow between runs; the load-bearing number is the zero.",
"note": "Both code sites exist verbatim in the current tree: src/recover/events.rs lines 35-41 and src/recover/scan.rs lines 400-406 match the quoted snippets character for character. The update-side measurement is new here and makes the claim stronger than it was written: the omission is not specific to creates, it covers every landed file tool in the lane."
}
]
},
{
"id": "SUB-027",
"area": "subagents",
"behavior": "The 2.1.191 floor holds: across 645 landed file results recorded below 2.1.191 in the built-in lane (49 creates + 596 updates) not one carries the structured toolUseResult echo. The 'onward' half is not absolute: 57 landed file results at 2.1.191 or later carry no echo (1 create at 2.1.199, 3 creates and 54 updates at 2.1.231), and they are interleaved with echo-bearing results inside the same four transcripts - so echo presence is a per-operation property, not a per-lane or per-version one.",
"depends": "csift `recover` gates its input-side fallback per tool_use ID rather than per lane, so a built-in subagent transcript reconstructs from the carrier exactly like a top-level session and never double-emits; a lane-level gate would double-count every built-in subagent write.",
"code": [
{
"path": "src/recover/scan.rs",
"lines": "400-404",
"snippet": " // tool_use_ids whose result carrier carries the structured `toolUseResult` echo -\n // i.e. the ops `extract_from_tool_use_result` can reconstruct from. SUBAGENT and\n // workflow-agent transcripts OMIT `toolUseResult` (the tool_result is just a\n // `\"File created successfully…\"` string), so those ids are absent here and the\n // input-side fallback below supplies their content (§ subagent recover)."
}
],
"instrument": "The phrase 'all at version >= 2.1.191 bar 4 strays' is inverted. No echo-bearing block sits below 2.1.191 at all; the 4 create-side strays are echo-LESS blocks at 2.1.191 or later (1 at 2.1.199, 3 at 2.1.231).",
"located": {
"claude_code": "2.1.258",
"csift": "0.2.0",
"source": "measured 2026-09-02; dev session 2026-08-23"
},
"first_seen_claude_code": "2.1.191",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY' # os.walk ~/.claude/projects, skip journal.jsonl; lane from path components (has 'workflows' => workflow, has 'subagents' only => built-in, neither => top level); parse a line only when it holds the marker bytes; for every tool_result BLOCK whose rendered text holds the marker, tally (lane, isinstance(rec['toolUseResult'], dict)) PY (restricted to '<uuid>/subagents/<name>.jsonl', i.e. no 'workflows' component, and bucketed additionally by the record's 'version' field and by the sibling '<stem>.meta.json' taskKind)",
"observed": "Built-in-location create results: 853 blocks, 800 with the dict echo and 53 without - both totals reproduce the claim exactly. Version split of the 800 echo-bearing blocks: 2.1.191 24, 2.1.196 37, 2.1.199 32, 2.1.208 18, 2.1.210 18, 2.1.211 4, 2.1.217 247, 2.1.219 126, 2.1.231 235, 2.1.233 7, 2.1.251 50, 2.1.252 1, 2.1.257 1 - every one at 2.1.191 or later, none below. The 53 echo-less: 2.1.156 2, 2.1.159 3, 2.1.177 44, 2.1.199 1, 2.1.231 3. Extension to update results in the same lane (teammate transcripts excluded): below 2.1.191 596 blocks, 596 echo-less (2.1.159 14, 2.1.177 582); at 2.1.191 or later 638 with the echo but 54 without, all at 2.1.231 and confined to 4 built-in transcripts that also hold echo-bearing results - in one of them the 12 echo-bearing update results run 13:12 to 14:11 and the 10 echo-less ones run 14:47 to 14:51, same transcript, same record version.",
"rule": "One count per tool_result BLOCK whose text holds the create or the update marker, bucketed by the carrying record's own 'version' string and by whether its toolUseResult is a dict. Built-in lane = a 'subagents' path component with no 'workflows' component; teammate transcripts (sibling meta taskKind in_process_teammate) excluded from the update extension so the two claims stay separable.",
"note": "src/recover/scan.rs lines 400-404 match the quoted snippet verbatim. The interleaving is direct evidence for the claim's own 'depends': a lane-level gate would be wrong in both directions here, while the per-tool_use-id gate csift actually uses handles a transcript that mixes both shapes."
}
]
},
{
"id": "SUB-028",
"area": "subagents",
"behavior": "A TEAMMATE lane (a built-in-location transcript whose `*.meta.json` carries `taskKind:\"in_process_teammate\"`) always carries the structured `toolUseResult` echo on a file write - 491 of 491 create results in the corpus.",
"depends": "csift attributes teammate file mutations through the same carrier path as a top-level session, so `agents --with-files` and `files` report a teammate's creates with an accurate `is_create` without ever reaching the input-side fallback.",
"code": [
{
"path": "src/model/mutation.rs",
"lines": "186-192",
"snippet": " pub fn carrier_create_paths(&self) -> Vec<(String, String, bool)> {\n let Some(probe) = self.tur_probe() else {\n return Vec::new();\n };\n let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {\n return Vec::new();\n };"
}
],
"instrument": "python3 over `~/.claude/projects/**/subagents/*.jsonl`: read the sibling `<stem>.meta.json`, take its `taskKind`, and bucket each `File created successfully at:` tool_result by (taskKind, echo-present). Counting rule: one count per matching tool_result BLOCK. Observed `('in_process_teammate', True)` 491 and `('in_process_teammate', False)` 0.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "python3 - <<'PY' # os.walk ~/.claude/projects, skip journal.jsonl; lane from path components (has 'workflows' => workflow, has 'subagents' only => built-in, neither => top level); parse a line only when it holds the marker bytes; for every tool_result BLOCK whose rendered text holds the marker, tally (lane, isinstance(rec['toolUseResult'], dict)) PY (built-in-location transcripts only; taskKind read from the sibling '<stem>.meta.json'; bucket every create and every update tool_result by (taskKind, dict-valued toolUseResult))",
"observed": "Teammate lanes (sibling meta taskKind == in_process_teammate): 491 'File created successfully at:' blocks, 491 with a dict-valued toolUseResult, 0 without - the claim's 491/491 reproduces exactly. Extension not in the claim: 1357 update results in the same lanes, 1357 with the echo, 0 without. 1848 landed file results, 0 counterexamples.",
"rule": "One count per tool_result BLOCK whose text holds the create or update marker; taskKind is read once per transcript from the sibling meta file; a transcript with no meta or no taskKind is bucketed separately and is not counted here.",
"note": "src/model/mutation.rs lines 186-192 match the quoted snippet verbatim. The teammate lane is the only subagent-located lane in the corpus with a perfect echo record; the general-purpose built-in lane has 57 exceptions (see SUB-027) and the workflow lane has none at all (SUB-026)."
}
]
},
{
"id": "SUB-029",
"area": "subagents",
"behavior": "A file READ performed inside a subagent lane is usually persisted as a bare rendered `tool_result` string with NO `toolUseResult` field on the record at all, whereas a top-level session always writes the structured echo: pairing every `Read` tool_use with its later same-id tool_result across the corpus gives 6071 top-level pairs carrying a `file` echo, 84 other and 0 missing, against 3129 subagent pairs carrying a `file` echo, 626 other and 23183 missing.",
"depends": "csift `recover` reconstructs content only from the structured echo plus an input-side fallback covering Write/Edit/MultiEdit, so a Read inside a subagent lane contributes no content anchor and `recover --file` reports far less coverage than the transcript actually holds.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "35-41",
"snippet": "/// WHY: a subagent (built-in Task/Agent-tool) and a workflow-agent transcript record the\n/// tool RESULT as a bare `tool_result` string (`\"File created successfully at: …\"`) with\n/// NO structured `toolUseResult` echo - unlike a top-level session, whose carrier carries\n/// `{type:create, filePath, content, …}`. `extract_from_tool_use_result` reads that echo,\n/// so without this fallback a file WRITTEN BY A SUBAGENT is invisible to `recover`\n/// (`no recoverable history`) even though `files`/`search` see it (they read the tool_use\n/// input directly). The authoritative content IS in the input - `Write.content`,"
},
{
"path": "src/recover/scan.rs",
"lines": "400-406",
"snippet": " // tool_use_ids whose result carrier carries the structured `toolUseResult` echo -\n // i.e. the ops `extract_from_tool_use_result` can reconstruct from. SUBAGENT and\n // workflow-agent transcripts OMIT `toolUseResult` (the tool_result is just a\n // `\"File created successfully…\"` string), so those ids are absent here and the\n // input-side fallback below supplies their content (§ subagent recover).\n let mut ids_with_result: std::collections::HashSet<String> =\n std::collections::HashSet::new();"
}
],
"instrument": "Re-measured 2026-09-02 at Claude Code 2.1.258: top level 6037 with a file echo, 82 other, 0 missing; subagent 3129 with a file echo, 626 other, 23230 missing. The subagent 'file' and 'other' buckets reproduce the claim's 3129 and 626 exactly, so the pairing rule is the same one; 'missing' grew 23183 -> 23230 with new activity. The top-level buckets came in LOWER than the claim's 6071 / 84, so top-level counts are not monotone - a top-level transcript can leave the corpus - and only the 0-missing result is stable.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02"
},
"first_seen_claude_code": "2.1.156",
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY' # single forward pass per jsonl under ~/.claude/projects: collect ids of tool_use blocks named Read, then for the later tool_result block carrying that id classify the CARRYING record as 'file' when toolUseResult.file is an object, 'other' when toolUseResult is present in another shape, 'missing' when the field is absent or null; bucket by whether the path has a 'subagents' component; also tally the carrying record's version PY",
"observed": "Top level 6037 'file' / 82 'other' / 0 'missing'. Subagent 3129 'file' / 626 'other' / 23230 'missing'. Restricted to records stamped version 2.1.258 (the current release): top level 103 'file' and 0 'missing'; subagent 18 'file' and 248 'missing'. The subagent 'missing' bucket spans record versions 2.1.156 through 2.1.258, so it is current behavior.",
"rule": "One observation per (Read tool_use, first later tool_result with the same id) pair inside one file; an unreturned Read contributes nothing; scope is the path test alone.",
"note": "The 82 top-level 'other' pairs are Reads whose echo is not a file object (an image read, for one). The claim's 'always writes the structured echo' survives in the strong form that matters: 0 top-level Read pairs of 6119 have no toolUseResult at all, at every version from 2.1.150 to 2.1.258."
}
]
},
{
"id": "SUB-030",
"area": "subagents",
"behavior": "A workflow-agent transcript writes a `toolUseResult` field ONLY for a FAILED tool call, and always as a plain JSON STRING (the error text), never the structured object: across every workflow lane in the corpus `toolUseResult` is present on exactly 2592 records, all string-typed, and every one of those records also carries a `tool_result` block with `is_error:true` - a 1:1 correspondence.",
"depends": "csift derives `ids_with_result` from `rec.tool_use_result.is_some()`, which is true for those string-valued error carriers too, so the input-side fallback would skip them; they are also in `failed_ids`, so the two gates agree and a failed workflow-lane Edit is never replayed as a phantom mutation.",
"code": [
{
"path": "src/recover/scan.rs",
"lines": "417-424",
"snippet": " let has_structured_result = rec.tool_use_result.is_some();\n if let Some(blocks) = rec.blocks() {\n for b in blocks {\n if let Block::ToolResult {\n tool_use_id: Some(id),\n is_error,\n ..\n } = b"
}
],
"instrument": "Re-measured 2026-09-02: 2594 records, 2594 string-typed, 2594 with an errored tool_result block, and the converse 2594 of 2594 - the claim's 2592 was the same measurement a few hours earlier on a live corpus.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY' # os.walk ~/.claude/projects restricted to paths containing a 'workflows' component, skip journal.jsonl; for every record holding b'\"tool_result\"' tally (block['is_error'] is True, json type of block['content']) per BLOCK and (record has an errored tool_result, record['toolUseResult'] is not None) per RECORD PY",
"observed": "Workflow lanes: 2594 records carry a non-null toolUseResult; the JSON type is string on 2594 of 2594 and object on 0; all 2594 also carry a tool_result block with is_error true. Converse direction, which the claim asserts but did not measure: of the workflow records carrying at least one errored tool_result block, 2594 of 2594 carry a toolUseResult - so the correspondence is 1:1 in both directions.",
"rule": "One count per RECORD. A record qualifies for the first tally when 'toolUseResult' is present and not null; for the second when any of its message.content blocks has type tool_result and is_error true.",
"note": "src/recover/scan.rs lines 417-424 match the quoted snippet verbatim. Because the string-valued error carrier still makes rec.tool_use_result.is_some() true, the id lands in ids_with_result AND in failed_ids, so the two gates agree exactly as the claim's 'depends' says."
}
]
},
{
"id": "SUB-031",
"area": "subagents",
"behavior": "The first half holds: 116528 of 119112 workflow tool_result blocks carry a bare string (97.8%). The second half is backwards. The 2584 array-valued blocks are not the errored calls - none of them is errored. The array form marks a non-text payload (2447 deferred tool-schema fetches carrying 'tool_reference' blocks, 133 image reads carrying 'image' blocks, 4 text), while an ERROR always arrives as a string: 2594 of 2594 errored workflow tool_results are string-valued.",
"depends": "csift normalises both shapes through `model::tool_result_content_text` before matching, so the integrity-error classifier and `search`'s tool-result rendering behave identically in either lane; a surface that assumed the array form would see no text at all in workflow transcripts.",
"code": [
{
"path": "src/recover/carriers.rs",
"lines": "284-285",
"snippet": "pub(crate) fn classify_integrity_error(content: &serde_json::Value) -> Option<IntegrityKind> {\n let text = crate::model::tool_result_content_text(content);"
}
],
"instrument": "Add the is_error cross-tab, otherwise the two totals (2584 arrays vs 2594 errors) invite the coincidence reading that this measurement refutes.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "python3 - <<'PY' # os.walk ~/.claude/projects restricted to paths containing a 'workflows' component, skip journal.jsonl; for every record holding b'\"tool_result\"' tally (block['is_error'] is True, json type of block['content']) per BLOCK and (record has an errored tool_result, record['toolUseResult'] is not None) per RECORD PY ; plus a second workflow pass joining each tool_result back to its tool_use id to name the originating tool for the array-valued blocks",
"observed": "Workflow tool_result content types: 116477 string / 2584 array on the first pass and 116528 string / 2584 array eight minutes later (the corpus is live; the array count did not move). Cross-tab with is_error: (is_error false, array) 2584, (is_error false, string) 113934, (is_error true, string) 2594, (is_error true, array) 0. Every errored workflow tool_result carries a STRING content and every array-valued content is non-errored. What the arrays actually are, by the name of the tool_use they join back to: 2447 carrying only 'tool_reference' blocks (deferred tool-schema fetch results), 133 carrying only 'image' blocks (image reads), 4 carrying 'text' (MCP tool results). 2447 + 133 + 4 = 2584.",
"rule": "One count per tool_result BLOCK in a workflow-lane transcript; content type is the JSON type of block['content']; the originating tool name comes from the tool_use with the same id earlier in the same file.",
"note": "src/recover/carriers.rs lines 284-285 match the quoted snippet verbatim, and the normalisation argument the claim rests on is unaffected - a surface that assumed the array form would miss 97.8% of workflow tool results, and a surface that assumed the string form would miss every image and tool-schema payload."
}
]
},
{
"id": "SUB-032",
"area": "subagents",
"behavior": "Claude Code REJECTS a `Write` issued from inside ANY agent lane when the basename matches the report-file pattern - the guard is an `agentId` present together with `/^(REPORT|SUMMARY|FINDINGS|ANALYSIS).*\\.md$/i` - with errorCode 5 and the message `Subagents should return findings as text, not write report files. Include this content in your final response instead.`, delivered as a `<tool_use_error>`-wrapped tool_result with `is_error:true`.",
"depends": "csift's `failed_ids` gate is what keeps that rejected Write out of both surfaces: without it the input-side fallback would replay the report's full content as a created file and `files` would report a write that `recover` correctly finds no history for - a forensic false positive on 'did this agent write X?'.",
"code": [
{
"path": "src/recover/events.rs",
"lines": "67-76",
"snippet": " // Skip when this op already has a `toolUseResult` carrier to reconstruct from, OR\n // when its result was an ERROR (a failed Edit/Write never mutated the file, so its\n // input is a phantom - `is_error:true` covers both \"String to replace not found\"\n // and the Edit-before-Read \"File has not been read yet\" wall, incl. the\n // Bash-created-then-directly-Edited and the must-re-Read-a-plan cases).\n if let Some(id) = id {\n if ids_with_result.contains(id) || failed_ids.contains(id) {\n continue;\n }\n }"
}
],
"instrument": "The bare phrase search is no longer a clean counting rule: 12 tool_result blocks corpus-wide hold the phrase today because the message has since been quoted in prose and in binary dumps. Anchor on the '<tool_use_error>' prefix plus is_error true, which still gives exactly 4 - all workflow lanes, all at record version 2.1.231.",
"located": {
"claude_code": "2.1.231",
"csift": "0.2.0",
"source": "measured 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search 'Subagents should return findings as text' -t agent.tool.result --format json ; python3 - <<'PY' # walk ~/.claude/projects, print lane, record version and block is_error for every tool_result whose text holds the phrase PY ; python3 -c \"b=open('~/.local/share/claude/versions/2.1.258'.replace('~',__import__('os').path.expanduser('~')),'rb').read(); print(b.count(b'tengu_subagent_md_report_blocked'), b.count(b'REPORT|SUMMARY|FINDINGS|ANALYSIS'))\"",
"observed": "python pass: 12 tool_result blocks corpus-wide now hold the phrase, of which exactly 4 are rejections (text begins '<tool_use_error>Subagents should return findings as text'), all with is_error true, all in workflow lanes, all on records stamped 2.1.231; the other 8 are later transcripts quoting the message as prose or dumping it out of a binary. csift agrees independently: matched 9, and exactly 4 hits render the '<tool_use_error>' head with is_error true. Binary 2.1.258 holds the guard once, inside a Write tool validateInput: 'if(n.agentId&&/^(REPORT|SUMMARY|FINDINGS|ANALYSIS).*\\.md$/i.test(...))return s(\"tengu_subagent_md_report_blocked\",{contentBytes:Buffer.byteLength(o)}),{result:!1,message:\"Subagents should return findings as text, not write report files. Include this content in your final response instead.\",errorCode:5}' - the literals 'tengu_subagent_md_report_blocked' and 'REPORT|SUMMARY|FINDINGS|ANALYSIS' each occur twice in the binary (once in the string table, once in the code).",
"rule": "One count per tool_result BLOCK. A block counts as a REJECTION only when its text starts with '<tool_use_error>' followed by the message and is_error is true; a block that merely contains the phrase does not.",
"note": "src/recover/events.rs lines 67-76 match the quoted snippet verbatim. The guard is CURRENT, not a fossil of 2.1.231: the 2.1.258 binary still carries it, gated on n.agentId being set, testing the basename with the case-insensitive regex, and returning errorCode 5. The corpus has no rejection after 2.1.231 simply because no agent has attempted such a filename since."
}
]
},
{
"id": "SUB-033",
"area": "subagents",
"behavior": "Claude Code itself reconstructs raw file content from the RENDERED side when it rebuilds read state out of a transcript: it strips every `<system-reminder>...</system-reminder>` block, maps each line through a gutter stripper matching leading digits followed by a tab, colon or U+2192 arrow, rejoins with newlines and then trims the result.",
"depends": "that is the exact inverse csift would need for the echo-less subagent Reads, and the trailing trim is why such a reconstruction is not byte-exact - a csift implementation must decide whether to copy the trim or diverge; csift's own gutter stripper already implements the per-line half.",
"code": [
{
"path": "src/recover/diff.rs",
"lines": "193-198",
"snippet": " // Find the gutter separator: a tab or the U+2192 arrow after leading digits.\n let trimmed = line.trim_start();\n let digits: String = trimmed.chars().take_while(char::is_ascii_digit).collect();\n if digits.is_empty() {\n continue;\n }"
}
],
"instrument": "Seek the literal `content.replace(/<system-reminder>` in a `strings -a` dump of the 2.1.258 Claude Code binary; the same expression continues with a split on newlines, a map through the gutter-stripping helper, a rejoin and a trim. The helper itself is a one-line function returning the first capture of `/^\\s*\\d+[\\u2192\\t:](.*)$/` or the line unchanged, findable by the same seek. Counting rule: one `strings` output line holds each minified expression; expect exactly one construction site.",
"located": {
"claude_code": "2.1.258",
"csift": null,
"source": "measured 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'content\\.replace\\(/<system-reminder>.{0,220}' ; python3 -c \"import os;b=open(os.path.expanduser('~/.local/share/claude/versions/2.1.258'),'rb').read();n=b'content.replace(/<system-reminder>';print(b.count(n));i=b.find(n);print(b[i-260:i+320]);m=b'function adr(';print(b.count(m));j=b.find(m);print(b[j:j+80])\"",
"observed": "Exactly one occurrence of 'content.replace(/<system-reminder>' in the 2.1.258 binary, reading in full: I.content.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/g,\"\").split(`\\n`).map(adr).join(`\\n`).trim(). Exactly one occurrence of 'function adr(', reading in full: function adr(e){return e.match(/^\\s*\\d+[\\u2192\\t:](.*)$/)?.[1]??e}. The enclosing gate is I.type===\"tool_result\"&&I.tool_use_id joined to a Read entry, I.is_error!==!0 and typeof I.content===\"string\"; the reconstructed text is then stored per filePath together with an isPartialView flag set when C.toolUseResult?.file?.truncatedByTokenCap===!0.",
"rule": "Byte count of each literal in the binary; the claim predicts exactly one construction site and exactly one helper, and both counts are 1.",
"note": "src/recover/diff.rs lines 193-198 match the quoted snippet verbatim, and csift's strip_gutter accepts the tab and the U+2192 arrow but not the colon separator the harness regex also allows - a deliberate narrowing, worth knowing if csift ever implements the full inverse. This site is also the mechanical link to SUB-029: it reads toolUseResult?.file when present and otherwise reconstructs from the rendered string, which is exactly the situation of an echo-less subagent Read."
}
]
},
{
"id": "QT-SYS-001",
"area": "queue-and-telemetry",
"behavior": "Claude Code writes `type:\"system\"` records for its own UI that carry a `subtype` outside the four csift models separately (`compact_boundary`, `turn_duration`, `away_summary`, `stop_hook_summary`): observed subtypes are `informational` (a notice with a `level` such as `warning` or `notice` and a string `content`, e.g. the Remote Control disconnect warning written when the signed-in account changed in another session), `agents_killed`, `scheduled_task_fire`, `model_refusal_fallback`, `model_refusal_no_fallback`, `api_error` and `local_command`; every one carries `uuid`, `timestamp` and `parentUuid`, `isMeta:false`, and NO `message{}` field.",
"depends": "`search -t harness.meta.system` (the v0.10.1 catch-all leaf, gated) classifies any system subtype other than the four modeled ones and renders `[<subtype> <level>] <content>`; `show --line` renders such a line flag-free; if the harness started sending these records to the model (a `message{}` field) or moved a subtype into a differently-typed record, the leaf's visibility law and its candidate needle would be wrong.",
"code": [
{
"path": "src/model/classify_promoted.rs",
"lines": "24-25",
"snippet": "\"compact_boundary\" => None,\n _ => Some(Class::MetaSystem),"
},
{
"path": "src/search/record_text.rs",
"lines": "245",
"snippet": "let subtype = rec.subtype.as_deref().unwrap_or(\"system\");"
},
{
"path": "src/search/scan.rs",
"lines": "377",
"snippet": "std::sync::LazyLock::new(|| memmem::Finder::new(b\"\\\"subtype\\\"\"));"
}
],
"instrument": "Corpus: rg --no-filename -o '\"type\":\"system\",\"subtype\":\"[a-z_]+\"' -- <project-dir>/*.jsonl | sort | uniq -c (count every subtype; the four modeled ones aside, every other value must classify under harness.meta.system: csift search '' <target> -t harness.meta.system --count-by label). Binary: strings -n 6 <claude-binary> | rg -c 'subtype:\"informational\"|\"informational\"' (and the same for each other subtype) plus rg -o 'subtype:\"informational\"[^}]{0,120}' to read the writer's field set (content, level). Visibility: none of these records carries a message{} field (jq 'select(.type==\"system\") | has(\"message\")' over the transcript).",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.1",
"source": "SPEC.md v0.10.1 ledger entry 9; AGENTS.md section 3.3a gated-promoted bullet; measured now"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "rg --no-filename -o '\"type\":\"system\",\"subtype\":\"[a-z_]+\"' -- <csift project dir>/*.jsonl | sort | uniq -c; jq on the live informational record (type, subtype, level, content, isMeta, keys); strings -n 6 <claude 2.1.258 binary> | rg -c '\"<subtype>\"' per subtype and rg -o 'subtype:\"informational\"[^}]{0,120}'; ./target/debug/csift search '' @<session> --no-subagents -t harness.meta.system",
"observed": "Project-dir census: stop_hook_summary 412, turn_duration 286, away_summary 147, compact_boundary 17, agents_killed 3, scheduled_task_fire 1, model_refusal_fallback 1, informational 1. The live informational record: type system, subtype informational, level warning, isMeta false, string content (the Remote Control disconnect notice), keys content,cwd,entrypoint,gitBranch,isMeta,isSidechain,level,parentUuid,sessionId,slug,subtype,timestamp,type,userType,uuid,version - no message key. Binary literal counts: informational 17, api_error 29, model_refusal_fallback 19, model_refusal_no_fallback 7, agents_killed 4, local_command 11, scheduled_task_fire 9; two informational writer sites read subtype:\"informational\",content:<fn>(e),level:\"notice\". csift search -t harness.meta.system on the live session: 2 hits ([agents_killed] and [informational warning] Remote Control disconnected ...), matched 2 exchanges.",
"rule": "One count per raw line matching the exact type+subtype pair over the project directory's top-level transcripts (subagent lanes excluded); binary counts are occurrences of the quoted subtype string among strings of length >= 6; the visibility fact is the key set of the one live record.",
"note": "The four modeled subtypes keep their own leaves; a system line with no subtype stays unlabeled by design. level values seen: warning (live record), notice (binary writer sites)."
}
]
},
{
"id": "TURN-001",
"area": "turn-boundary",
"behavior": "A `type:\"user\"` record is not always a human turn: `tool_result` blocks ride on `role:\"user\"` records too, so most user records are tool-result CARRIERS rather than operator prose. Re-measured 2026-09-02 over 64 top-level transcripts: 59,005 of 67,758 `role:\"user\"` records are carriers (87.1%) against 3,289 that pass the genuine shape gate - a 20.6x corpus-wide overcount for a bare `role:\"user\"` filter, rising to 119.6x on the most agent-heavy session. On one 47,602-line transcript the naive filter reports 3,604 where csift reports 77 `user.message` records (46.8x).",
"depends": "`Record::is_genuine_user` gates on content SHAPE - a bare string, or blocks carrying a `text` block and NO `tool_result` block - and is the discriminator behind the `user.message` label, turn delimiting, `list` previews and `verbatim` reconstruction. A bare `role:\"user\"` filter overcounts human turns about 20x corpus-wide and about 120x on the carrier-heaviest session.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "37-43",
"snippet": " pub fn is_genuine_user(&self) -> bool {\n if !self.is_type(\"user\") {\n return false;\n }\n if self.is_compact_summary.unwrap_or(false) {\n return false;\n }"
},
{
"path": "src/model/predicates.rs",
"lines": "68-75",
"snippet": " match &msg.content {\n Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n Some(Content::Blocks(blocks)) => {\n let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n if !has_text || has_tool_result {\n return false;\n }"
}
],
"instrument": "`rg -c '\"role\":\"user\"' <transcript>` (the carrier-inflated raw count) versus `csift search '' @<id> -t user.message -c` (the genuine count); counting rule: one per physical line for rg, one per record passing `is_genuine_user` for csift. Split the raw side by shape with `jq -r 'select(.type==\"user\") | (if (.message.content|type)==\"string\" then \"string\" elif ([.message.content[]?.type]|index(\"tool_result\")) then \"tool_result\" else \"text\" end)' <transcript> | sort | uniq -c`.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md section 3.3; SPEC.md section 4.1; SKILL.md wrong-assumption table"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) csift search '' --count-by label --no-subagents ; (b) rg -c '\"role\":\"user\"' ~/.claude/projects/<proj>/<id>.jsonl and csift search '' @<id> --no-subagents --count-by label ; (c) python3 over every top-level transcript: for each line with '\"role\"', json.loads, keep message.role=='user', tally (i) all such records, (ii) those whose message.content is a list containing a tool_result block, (iii) those passing the is_genuine_user shape gate re-implemented in python (not isMeta, not isCompactSummary, string content OR blocks with a text block and no tool_result block, minus the interrupt/local-command-stdout/slash-wrapper/task-notification markers and the peer wrappers)",
"observed": "Corpus (64 top-level transcripts): 67758 role:user records; 59005 of them are tool_result carriers = 87.1%; 3289 pass the genuine shape gate (2642 string-content + 647 text-block). Naive-filter overcount factor = 67758/3289 = 20.60x corpus-wide; the worst single session (>=20 genuine) is 6580 role:user vs 55 genuine = 119.6x, and the largest session is 13265 role:user vs 112 genuine = 118.4x with 91.0% carriers. csift's own top-level census reports 2468 user.message + 848 user.unsent = 3316 records, which brackets the python gate's 3289 (csift additionally splits out superseded drafts and the agents-stopped notice). Single-transcript run of the ledger's own instrument: rg -c '\"role\":\"user\"' = 3604 on a 47602-line transcript, against csift user.message = 77 on the same file, i.e. 46.8x.",
"rule": "One per JSON record (line). 'carrier' = message.role=='user' AND message.content is an array containing a block with type=='tool_result'. 'genuine' = the is_genuine_user shape gate above. csift's user.message count is one per classified record. Top-level transcripts only (subagent files excluded) so the rg and csift sides address the same bytes.",
"note": "The trap is confirmed and has grown considerably sharper than the recorded figures - the direction, the mechanism and the shape gate are all intact, only the magnitudes were stale. The claim's per-session example pairs (393/1619 and 177/8675) could not be matched to specific transcripts in the present corpus, so they were replaced with freshly measured pairs carrying their counting rule. Code sites src/model/predicates.rs:37-43 and 68-75 both match verbatim."
}
]
},
{
"id": "TURN-002",
"area": "turn-boundary",
"behavior": "Some human turns carry no `text` block at all - the operator's words ride inside a `tool_result` payload (an AskUserQuestion answer, a tool-use rejection with a typed instruction). Re-measured 2026-09-02 over 64 top-level transcripts: 126 of 2,594 human turn openers (4.9%) extract as ZERO characters when only `text` blocks are read. A reader that walks only `content[].type==\"text\"` blocks additionally loses the 2,642 of 3,289 genuine openers (80%) whose content is a bare string rather than a block array.",
"depends": "csift recovers those turns through `user.answer` / `user.rejection` (each dual-labeled with `agent.tool.result` and deduped to the richer user view) and renders the opener body through `Record::reconstructed_user_text`, so a text-block-only reader is the thing that loses the words, not csift.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "235-250",
"snippet": " pub fn reconstructed_user_text(&self, plan_index: Option<&PlanIndex>) -> Option<String> {\n if let Some(text) = self.genuine_user_text() {\n return Some(text);\n }\n if let Some(unit) = self.auq_exchange() {\n return Some(normalize_line(&unit));\n }\n if let Some((rejected_id, msg)) = self.plan_rejection_message() {\n let mut out = normalize_line(&msg);\n if let (Some(idx), Some(id)) = (plan_index, rejected_id.as_deref()) {\n if let Some(path) = idx.plan_path(id) {\n out.push_str(&format!(\" [plan: {path}]\"));\n }\n }\n return Some(out);\n }"
},
{
"path": "src/model/predicates.rs",
"lines": "68-75",
"snippet": " match &msg.content {\n Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n Some(Content::Blocks(blocks)) => {\n let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n if !has_text || has_tool_result {\n return false;\n }"
}
],
"instrument": "`csift search '' @<id> --count-by label` and compare `user.answer` + `user.rejection` against `user.message`; counting rule: one per record, richest-view deduped. Cross-check by extracting only `text` blocks from each turn opener with jq and counting the empty results.",
"located": {
"claude_code": null,
"csift": "0.8.2",
"source": "SKILL.md wrong-assumption table; AGENTS.md section 3.3"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' --count-by label --no-subagents (whole-scope record census, reading the user.message / user.answer / user.rejection keys), cross-checked against the python genuine-shape census from TURN-001 which additionally splits genuine openers into string-content and text-block-content",
"observed": "Top-level census: user.message 2468, user.answer 116, user.rejection 10 records. Human turn openers = 2468+116+10 = 2594; the openers whose words ride inside a tool_result payload and so yield ZERO characters to a text-block-only reader = 116+10 = 126, i.e. 4.86%. Separately, of the 3289 genuine openers found by the python shape gate, 2642 (80.3%) carry STRING content rather than a `text` block, so a reader that literally walks only `content[].type==\"text\"` loses those as well.",
"rule": "One per classified record. 'human turn opener' = the three human opener leaves user.message + user.answer + user.rejection (peer-message and notification openers excluded - they are not the operator). 'zero characters from text blocks' = user.answer + user.rejection, which ride tool_result carriers and carry no text block by construction. Top-level transcripts only.",
"note": "The mechanism holds - the words really do hide in tool_result payloads and csift really does recover them through user.answer / user.rejection - but the recorded 197/1191 = 16.5% is stale by a factor of three; the present corpus reads 126/2594 = 4.9%. The share is corpus-dependent (it tracks how much a corpus uses AskUserQuestion), so the counting rule matters more than the percentage. Worth recording alongside it: the string-content share is the larger hazard for a blocks-only reader, and the claim did not mention it. Code sites src/model/exchange.rs:235-250 and src/model/predicates.rs:68-75 both match verbatim."
}
]
},
{
"id": "TURN-003",
"area": "turn-boundary",
"behavior": "A turn opens on any of FOUR record shapes, not just genuine human prose: a genuine user record, an ANSWERED AskUserQuestion whose answer rides a non-errored `tool_result` carrier, a tool-use rejection carrying a typed instruction, and an inbound peer message.",
"depends": "`Record::opens_turn` is the single delimiter every csift surface keys on (`search` exchange reconstruction, `show --turn`, `stats` turns, `files` attribution, `verbatim`); keying on `is_genuine_user` alone drops three of the four shapes and renumbers every `tN` header, `show --turn` address and `--turn` window in the crate.",
"code": [
{
"path": "src/model/exchange.rs",
"lines": "201-205",
"snippet": " /// The single boundary predicate (§6.4): this record opens a new turn iff it is a\n /// genuine human message, an ANSWERED AskUserQuestion (the answer is the user's\n /// message), a tool-use rejection carrying a typed user instruction, OR an inbound\n /// teammate/peer message. Every surface (turns / search / recover / files) keys turn\n /// delimiting on THIS predicate so they never drift."
},
{
"path": "src/model/exchange.rs",
"lines": "214-219",
"snippet": " pub fn opens_turn(&self) -> bool {\n self.is_genuine_user()\n || self.is_auq_answer_boundary()\n || self.is_plan_rejection_boundary()\n || self.is_peer_message_record()\n }"
}
],
"instrument": "`csift show @<id> --turn -3..` on a session that used AskUserQuestion or a plan rejection - the fetched turns must start at those record shapes - then `csift search '' @<id> -t user.answer -c` and `-t user.rejection -c` for the non-genuine openers. Counting rule: one turn per opening record, superseded drafts excluded from numbering.",
"located": {
"claude_code": "2.1.150",
"csift": "0.2.0",
"source": "AGENTS.md section 3.3; SPEC.md section 4.1; SKILL.md Labels section"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search '' -t <leaf> --no-subagents --max-count 0 --format json | jq -rc 'select(.kind==\"exchange\")|[.session_id,.turn_index]|@tsv' to locate a turn opened by each non-genuine shape, then csift show @<id> --turn <N> to read the turn back and inspect which record is first",
"observed": "All four opener shapes were found opening real turns. (1) genuine: 2468 user.message records top-level. (2) answered AskUserQuestion: turn 35 of one transcript renders its FIRST record as `t35 ... 0x25C2 user.answer L4102 [AskUserQuestion . 2 questions] Q1 ... A1: ...` - the answer carrier is the opener. (3) rejection with a typed instruction: turn 21 of another transcript renders its FIRST record as `t21 ... 0x25C2 user.rejection L1686 The user wants to clarify these questions. ...`. (4) inbound peer message: turn 33 of a third transcript renders its FIRST record as `t33 ... 0x25B8 agent.communication.inbox <name> => self L6568 <teammate-message teammate_id=...>`. Corpus-wide top-level counts for the three non-genuine shapes: user.answer 116, user.rejection 10, agent.communication.inbox 749 records.",
"rule": "One turn per opening record. A shape counts as confirmed only when `csift show --turn N` prints that record as the FIRST line of turn N, which is exactly what `opens_turn` decides; superseded drafts are outside turn numbering and were not counted.",
"note": "Confirmed by direct observation of all four shapes in live 2.1.258-era data, not by reading the predicate. The rejection specimen is worth recording: on an AskUserQuestion the harness synthesizes a clarify-request body ('The user wants to clarify these questions...') and delivers it behind the same typed-instruction delimiter, so the rejection arm fires for AskUserQuestion clarifications as well as plan kick-backs. Code sites src/model/exchange.rs:201-205 and 214-219 both match verbatim."
}
]
},
{
"id": "TURN-004",
"area": "turn-boundary",
"behavior": "`isMeta:true` on a `type:\"user\"` record is an AUTHORSHIP flag: the string content LOOKS human but is harness-injected. Re-measured 2026-09-02 over 64 top-level transcripts: 1,409 such records, led by `[Image: source: ...]` (628), `<local-command-caveat>...` (161), a skill base-directory preamble (41), `Stop hook feedback: ...` (30) and `Continue from where you left off.` (6). Claude Code 2.1.258 ships the continuation marker in three lengths - the bare `Continue from where you left off.` plus a process-restart variant and a runner-moved variant that both continue past the full stop - so the marker is a PREFIX, not always the whole content.",
"depends": "csift gates `isMeta` out of `is_genuine_user` before any content inspection, so a meta pseudo-turn never opens a turn and never enters the `user.message` census - verified directly: zero of the corpus's `user.message` records carry `isMeta:true`. The recognized markers reparent to `harness.schedule.*` / `harness.meta.*`; the continuation test is `trim_start().starts_with(...)`, which is what makes it survive the two longer 2.1.258 variants. An `isMeta` record matching no known marker classifies EMPTY - such a record is not merely unlabeled but invisible to a scan (40 of 41 skill-preamble records return no hit at all), so a novel hook wrapper can never be mistaken for the operator.",
"code": [
{
"path": "src/model/record.rs",
"lines": "70-75",
"snippet": " /// System-injected pseudo-turn marker (§4.2). `true` ⇒ a `type:\"user\"` record\n /// whose string/text content LOOKS human (\"Continue from where you left off.\",\n /// loop ticks, stop-hook feedback, `<local-command-caveat>…`) but is machine-\n /// generated - must be excluded from genuine-user and from turn-delimiting.\n #[serde(default, rename = \"isMeta\")]\n pub is_meta: Option<bool>,"
},
{
"path": "src/model/predicates.rs",
"lines": "44-47",
"snippet": " // §4.2 TRAP: isMeta:true user records look human but are system-injected.\n if self.is_meta.unwrap_or(false) {\n return false;\n }"
},
{
"path": "src/model/markers.rs",
"lines": "187-190",
"snippet": "/// The fixed harness-injected continuation marker (GOLD §5) - `harness.schedule.continuation`.\n/// A `type:\"user\"` (`isMeta`) record CC injects to resume a session from where it left off.\n/// Verified across real `~/.claude/projects` data (522 occurrences), exact content.\npub const SCHEDULE_CONTINUATION_MARKER: &str = \"Continue from where you left off.\";"
}
],
"instrument": "`rg -c '\"isMeta\":true' <transcript>` against `csift search '' @<id> --count-by label` (no `user.message` key may cover them), and `rg -c 'Continue from where you left off\\.' ~/.claude/projects/*/*.jsonl` against `csift search 'Continue from where you left off' -t harness.schedule.continuation -c`. Counting rule: one per raw line for rg, one per classified record for csift.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "AGENTS.md section 3.3; SPEC.md section 4.2; src/model/record.rs isMeta doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) csift search '' -t user.message --no-subagents --raw | rg -c '\"isMeta\":true' ; (b) python3 over every top-level transcript tallying type=='user' records with isMeta true, bucketed by the first 46 characters of their content; (c) csift search '<content head>' --count-by label --no-subagents for each named example; (d) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Continue from where you left off\\.[^\"\\\\`]{0,140}' | sort -u",
"observed": "(a) ZERO: not one user.message record in the whole top-level corpus carries isMeta:true. (b) 1409 isMeta:true user records exist top-level; the largest content heads are '[Image: source: ...]' 628, '<local-command-caveat>Caveat: The messages bel...' 161, a skill base-directory preamble 41, and assorted injected monitoring prompts. (c) 'Continue from where you left off.' -> 6 records, all isMeta, all classified harness.schedule.continuation, none user.message; 'Stop hook feedback' -> 30 isMeta records; '# Autonomous loop tick' -> 0 records in this corpus; the 41 skill-preamble isMeta records return only 1 matched record from search, i.e. 40 of them classify to no label at all and are invisible to a scan. (d) 2.1.258 ships THREE continuation strings, not one: 'Continue from where you left off.', 'Continue from where you left off. Note: this session was automatically restarted after its process exited unexpectedly; the user has not sent a new message since the restart', and 'Continue from where you left off. This session moved to a new runner; files you created earlier may no longer exist, so verify the working directory state before relying on'.",
"rule": "One per JSON record for the disk tallies; one per classified record for the csift censuses; one per distinct string for the binary. A named example counts as present only when a record's content STARTS with it (a mid-prose mention does not count).",
"note": "The core assertion is confirmed by the strongest available instrument - a raw sweep of every user.message record in the corpus finds no isMeta among them. Three corrections were needed. The '522 occurrences, exact content' figure is stale twice over: this corpus holds 6, and 2.1.258 emits two longer variants that make 'exact content' wrong as a description (csift's starts_with test already handles them, so no code change follows). '# Autonomous loop tick' does not occur in this corpus and was dropped from the example list. Code sites src/model/record.rs:69-74, src/model/predicates.rs:44-47 and src/model/markers.rs:174-177 all match verbatim."
}
]
},
{
"id": "TURN-005",
"area": "turn-boundary",
"behavior": "When the user interrupts, Claude Code writes a `type:\"user\"` text-block record whose content is EXACTLY one of two synthesized strings: `[Request interrupted by user]` or `[Request interrupted by user for tool use]`. Both strings are still present in the 2.1.258 binary. Re-measured 2026-09-02: 264 and 24 occurrences across 64 top-level transcripts (324 and 41 counting subagent transcripts too), all non-`isMeta`, all arriving as a text block, none carrying any extra prose and no third variant anywhere in the corpus.",
"depends": "csift matches them by exact `==` (never `contains`, so a real message quoting the phrase stays genuine), excludes them from `is_genuine_user` and from turn opening, and classifies them `harness.interrupt.user` / `harness.interrupt.tool`. Because no occurrence carries extra prose, dropping them as boundaries loses zero user content; counting them inflates every turn index downstream of an interrupt.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "81-90",
"snippet": "/// The two exact-content synthesized strings Claude Code writes when the user\n/// interrupts (§4.2.1). They are a `type:\"user\"` `text`-block record whose content is\n/// EXACTLY one of these - a machine-synthesized interrupt marker, NOT a human turn.\n/// Verified across real `~/.claude/projects` data: 116 + 21 occurrences, all\n/// non-`isMeta`, none carrying any extra prose (dropping them as turn boundaries loses\n/// zero user content).\npub const INTERRUPT_MARKERS: &[&str] = &[\n \"[Request interrupted by user]\",\n \"[Request interrupted by user for tool use]\",\n];"
},
{
"path": "src/model/markers.rs",
"lines": "47-52",
"snippet": "pub fn is_synthetic_user_marker(content: &str) -> bool {\n INTERRUPT_MARKERS.contains(&content)\n || content.starts_with(LOCAL_COMMAND_STDOUT_PREFIX)\n || is_slash_command_wrapper(content)\n || is_agents_stopped_notice(content)\n}"
}
],
"instrument": "`rg -oNI '\\[Request interrupted by user[^]]*\\]' ~/.claude/projects -g '*.jsonl' | sort | uniq -c` - expect exactly the two strings and no third variant - then `csift search 'Request interrupted by user' --count-by label`, where every hit must key under `harness.interrupt.*` and none under `user.message`. Counting rule: one per record whose content EQUALS the marker exactly (a mid-prose quote does not count).",
"located": {
"claude_code": "2.1.258",
"csift": "0.1.0",
"source": "AGENTS.md section 3.3; SPEC.md section 4.2.1; src/model/markers.rs INTERRUPT_MARKERS doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Request interrupted by user[^\"\\\\]*' | sort | uniq -c ; (b) python3 over every top-level transcript: for each type=='user' record join its string-or-text-block content and, when it starts with '[Request interrupted by user', bucket it by (exact-marker-or-NON-EXACT, isMeta, content shape) ; (c) csift search 'Request interrupted by user' --count-by label",
"observed": "(a) both strings are live in the 2.1.258 binary: 'Request interrupted by user]' and 'Request interrupted by user for tool use]'. (b) 264 records whose content is EXACTLY '[Request interrupted by user]' and 24 whose content is EXACTLY '[Request interrupted by user for tool use]'; ZERO records in the NON-EXACT bucket, i.e. no third variant and no occurrence carrying extra prose; all 288 are non-isMeta and all arrive as a text block rather than a bare string. (c) corpus-wide including subagent transcripts the label census reads harness.interrupt.user 324 and harness.interrupt.tool 41, with NO user.message key present among the 12 keys the pattern touches.",
"rule": "One per record whose joined content EQUALS the marker exactly - a mid-prose quote does not count, which is why the raw rg line count over the same dir (388 + 197) is far higher and unusable: this corpus's own dev sessions discuss the markers in prose and code.",
"note": "Every structural half of this claim survived an attempt to break it: the exact-match set is closed at two, the NON-EXACT bucket is empty, and no interrupt record classifies as the operator. Only the occurrence counts were stale (116/21 -> 264/24 top-level). The gap between the exact-content count and a raw regex count over the same directory (288 vs 585) is itself the argument for the exact-`==` test the code uses. Code sites src/model/markers.rs:76-85 and 42-47 both match verbatim."
}
]
},
{
"id": "TURN-006",
"area": "turn-boundary",
"behavior": "Local-command OUTPUT arrives as a non-`isMeta` `type:\"user\"` record whose string content STARTS WITH `<local-command-stdout>` (154 occurrences measured 2026-09-02 across 64 top-level transcripts), while its sibling wrapper `<local-command-caveat>` does carry `isMeta` (161 occurrences). The split is total in both directions - no stdout record carries the flag and no caveat record lacks it. Claude Code 2.1.258's tag registry also declares a third sibling, `local-command-stderr`, which produced no records in this corpus.",
"depends": "The `isMeta` gate alone does not catch the stdout form, so csift excludes it by content prefix and classifies it `harness.command.stdout`; without the explicit prefix test the harness's own command output opens a turn and is injected into a reconstructed turn as operator prose.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "92-96",
"snippet": "/// Prefix of a `<local-command-stdout>…` user record (§4.2.2) - local-command OUTPUT\n/// (machine), not the user's prose. Non-`isMeta` string content (its sibling\n/// `<local-command-caveat>` carries `isMeta` and is already excluded). Must NOT open a\n/// turn.\npub const LOCAL_COMMAND_STDOUT_PREFIX: &str = \"<local-command-stdout>\";"
},
{
"path": "src/model/classify.rs",
"lines": "273-276",
"snippet": " if s.starts_with(LOCAL_COMMAND_STDOUT_PREFIX) {\n push_unique(out, Class::CommandStdout);\n return;\n }"
}
],
"instrument": "`rg -c '\"content\":\"<local-command-stdout>' ~/.claude/projects/*/*.jsonl` and confirm none of those lines carry `\"isMeta\":true` while `<local-command-caveat>` lines do; then `csift search 'local-command-stdout' --count-by label`, which must key only under `harness.command.stdout`. Counting rule: one per record whose content string begins with the tag.",
"located": {
"claude_code": "2.1.258",
"csift": "0.1.0",
"source": "AGENTS.md section 3.3; SPEC.md section 4.2.2; src/model/markers.rs LOCAL_COMMAND_STDOUT_PREFIX doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) python3 over every top-level transcript: for each type=='user' record with string content, bucket by which of '<local-command-stdout>' / '<local-command-stderr>' / '<local-command-caveat>' it starts with, crossed with isMeta ; (b) csift search 'local-command-stdout' --count-by label and csift search '' -t harness.command.stdout --no-subagents --count-by label ; (c) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '<local-command-(stdout|caveat)>' and the surrounding tag registry",
"observed": "(a) 154 records begin with `<local-command-stdout>`, every one of them non-isMeta and string-content; 161 records begin with `<local-command-caveat>`, every one of them isMeta:true; ZERO records begin with `<local-command-stderr>`. The split is total - no stdout record carries isMeta and no caveat record lacks it. (b) csift's top-level census reports harness.command.stdout = 154, an exact match with the disk count, and the pattern census shows no user.message key. (c) `<local-command-stdout>` appears 16 times in the 2.1.258 binary and the emitting site is visible as um(`<local-command-stdout>${...}</local-command-stdout>`); the tag registry in the same binary reads wf=\"command-name\", Vp=\"command-message\", Wq=\"command-args\", Vd=\"local-command-stdout\", q_=\"local-command-stderr\", _B=\"local-command-caveat\".",
"rule": "One per record whose content string begins with the tag. csift side: one per classified record, top-level transcripts only so the two counts address the same bytes.",
"note": "The structural claim - the isMeta gate alone does not catch the stdout form, so a content-prefix test is required - is confirmed exactly, and csift's harness.command.stdout census agrees with the on-disk count to the record (154 = 154), with nothing keyed under user.message. Only the occurrence count was stale (52 -> 154). One thing the claim does not mention and a future reader should: 2.1.258 declares `local-command-stderr` beside the two known tags. It is unexercised here, so csift's marker set has no record to be wrong about yet, but a stderr-bearing record would today fall through the prefix test and read as the operator. Code sites src/model/markers.rs:87-91 and src/model/classify.rs:273-276 both match verbatim."
}
]
},
{
"id": "TURN-007",
"area": "turn-boundary",
"behavior": "A slash-command invocation is stored as a machine-templated wrapper record in TWO tag orders that coexist on disk: the older `<command-name>/x</command-name>...` leading form and the current `<command-message>...</command-message>\\n<command-name>/x</command-name>\\n<command-args>...` form with the message tag FIRST. Both tags still ship in Claude Code 2.1.258. Re-measured 2026-09-02 across 64 top-level transcripts: 44 files carry an old-order wrapper and 29 carry a new-order one, 20 files carry BOTH, and the record split is 161 old-order against 37 new-order.",
"depends": "`is_slash_command_wrapper` accepts EITHER leading tag, so a new-order wrapper is neither surfaced as genuine prose nor allowed to open a turn. Anchoring on `<command-name>` alone silently reclassified every new-order record as GENUINE user prose - raw wrapper XML surfaced as `user.message`, a no-args wrapper opened a turn, and turn NUMBERING shifted on every transcript carrying new-order wrappers.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "105-112",
"snippet": "/// The SAME slash-command wrapper in the NEWER tag order: current CC emits\n/// `<command-message>…</command-message>\\n<command-name>/x</command-name>\\n<command-args>…`\n/// -- the message tag FIRST (verified live 2026-07: 14 sessions new-order vs 35 old-order\n/// in one real corpus; both orders coexist). Detection must accept EITHER leading tag:\n/// anchoring on `<command-name>` alone silently reclassified every new-order record as\n/// GENUINE user prose - raw wrapper XML surfaced as `user.message`, and a no-args\n/// wrapper opened a turn.\npub const COMMAND_MESSAGE_PREFIX: &str = \"<command-message>\";"
},
{
"path": "src/model/markers.rs",
"lines": "114-118",
"snippet": "/// True when `content` is a slash-command wrapper (§4.2.3) in EITHER tag order.\n#[must_use]\npub fn is_slash_command_wrapper(content: &str) -> bool {\n content.starts_with(COMMAND_NAME_PREFIX) || content.starts_with(COMMAND_MESSAGE_PREFIX)\n}"
}
],
"instrument": "`rg -l '\"<command-message>' ~/.claude/projects -g '*.jsonl' | wc -l` versus `rg -l '\"<command-name>' ... | wc -l` (counting rule: distinct transcript FILES, each counted under the order its wrappers open with; a file spanning a version transition carries both), then `csift search '' @<id> --count-by label | grep harness.command.invocation`.",
"located": {
"claude_code": "2.1.150",
"csift": "0.5.0",
"source": "AGENTS.md section 3.3; SPEC.md section 4.2.3 and the v0.5.0 ledger; CHANGELOG 0.5.0"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "(a) python3 over every top-level transcript: for each type=='user' record with string content starting with `<command-name>` or `<command-message>`, record the file under the order its content OPENS with and tally the records ; (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '</?command-(name|message|args)>' | sort | uniq -c ; (c) csift show @<id> --line <n> --raw against the flag-free csift show @<id> --line <n> on a new-order record",
"observed": "(a) 44 files contain at least one old-order (`<command-name>` leading) wrapper and 29 files contain at least one new-order (`<command-message>` leading) wrapper, with 20 files carrying BOTH orders - direct evidence that the two coexist and that files span the transition. Record split: 161 old-order + 37 new-order = 198 wrapper records, which matches csift's harness.command.invocation census of 198 exactly. (b) 2.1.258 still ships all three tags: `<command-name>` 13, `</command-name>` 4, `<command-message>` 2, `<command-args>` 2, `</command-args>` 2. (c) a live new-order record's raw bytes read \"content\":\"<command-message>csift</command-message>\\n<command-name>/csift</command-name>\\n<command-args>what is the id of this session?</command-args>\" and csift renders it `user.message ... /csift what is the id of this session?` - recognized, not surfaced as prose.",
"rule": "Counting rule differs from the claim's and the difference is the point: a file is counted under EVERY order it contains, so the two file counts overlap by the 20 transition-spanning files (the claim counted each file under one order only, which cannot show the overlap). Records: one per JSON record whose content string begins with either tag.",
"note": "Both orders are alive in current data and in the current binary, and the 20 files carrying both are the cleanest possible demonstration that anchoring on a single leading tag would misread part of a single transcript. The recorded 14-vs-35 file figures are stale and were measured under a one-order-per-file rule that hides the overlap, so the counting rule is corrected alongside the numbers. Code sites src/model/markers.rs:100-107 and 109-113 both match verbatim."
}
]
},
{
"id": "TURN-008",
"area": "turn-boundary",
"behavior": "Genuine prose the user typed after a slash command is not in the record's visible content: it lives in the wrapper's `<command-args>...</command-args>` body, with the invoked command name in `<command-name>...</command-name>`.",
"depends": "csift recovers it with `Record::slash_command_args`, so a WITH-args wrapper carries BOTH labels `user.message` (first, richest-view law) and `harness.command.invocation` and its unfiltered render is the extracted `/name args` rather than wrapper XML; a NO-args wrapper is `harness.command.invocation` only. Without the extraction the typed instruction is unsearchable and buried in XML.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "100-105",
"snippet": " const OPEN: &str = \"<command-args>\";\n const CLOSE: &str = \"</command-args>\";\n let start = s.find(OPEN)? + OPEN.len();\n let end = s[start..].find(CLOSE).map_or(s.len(), |rel| start + rel);\n let args = s[start..end].trim();\n if args.is_empty() {"
},
{
"path": "src/model/classify.rs",
"lines": "277-288",
"snippet": " if is_slash_command_wrapper(s) {\n // Prose typed after the slash command (`<command-args>`) IS genuine user\n // input - and the RICHER view (richest-view law: the prose beats the\n // wrapper), so it is pushed FIRST: the unfiltered record-text emission\n // renders `/name args`, never the wrapper XML. An explicit\n // `-t harness.command.invocation` still reaches the wrapper form.\n if self.slash_command_args().is_some() {\n push_unique(out, Class::UserMessage);\n }\n push_unique(out, Class::CommandInvocation);\n return;\n }"
}
],
"instrument": "`csift search '<text typed after a slash command>' @<id>` must match, and `csift show @<id> --line <that line>` must render `/name args` rather than the raw XML; `csift search '' @<id> -t harness.command.invocation --format json | jq -r '.labels'` shows both labels on a with-args wrapper. Counting rule: records carrying the invocation leaf, split by whether `labels` also contains `user.message`.",
"located": {
"claude_code": null,
"csift": "0.5.0",
"source": "AGENTS.md section 3.3; SPEC.md section 4.2.3; src/model/classify.rs slash-wrapper arm"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search '' -t harness.command.invocation --no-subagents --max-count 0 --format json | jq -rc 'select(.kind==\"exchange\")|.hits[]|select(.label==\"harness.command.invocation\")|[(.labels|join(\"+\")),(.excerpt|test(\"<command-args></command-args>\")|tostring)]|@tsv' | sort | uniq -c ; then csift show @<id> --line <n> (flag-free) against csift show @<id> --line <n> --raw on one with-args record",
"observed": "198 wrapper records split exactly three ways: 157 labelled `harness.command.invocation` alone with an empty `<command-args></command-args>`, 4 labelled `harness.command.invocation` alone without that literal empty-args excerpt, and 37 labelled `user.message+harness.command.invocation` - user.message FIRST in every one of the 37. So 37 with-args wrappers carry both labels and 161 no-args wrappers carry the invocation leaf alone; the 37 agrees to the record with the independent python count of with-args wrappers (33 new-order + 4 old-order). The render law was checked on a live record whose raw bytes are `<command-message>csift</command-message>\\n<command-name>/csift</command-name>\\n<command-args>what is the id of this session?</command-args>`: flag-free `show` prints `0x25C2 user.message L<n> /csift what is the id of this session?`, the extracted `/name args`, while an explicit `-t harness.command.invocation` selector renders the raw wrapper XML instead.",
"rule": "One per classified record, top-level transcripts only. 'with-args' = the `<command-args>...</command-args>` body is non-empty after trimming; the label ORDER is read from the JSON `labels` array, whose first element is the richest view.",
"note": "Confirmed on every axis the claim asserts, by instrument rather than by reading the classifier: the dual label, the user.message-first ordering, the invocation-only label on no-args wrappers, and the `/name args` render replacing the XML. The 37/161 split reconciles exactly with the independent on-disk wrapper census from TURN-007 (198 total), so the two instruments cross-check each other. Code sites src/model/predicates.rs:100-105 and src/model/classify.rs:277-288 both match verbatim."
}
]
},
{
"id": "TURN-009",
"area": "turn-boundary",
"behavior": "Claude Code 2.1.258 synthesizes the AskUserQuestion answer written into the answering `tool_result` from a THREE-branch conditional, and the set has turned over since this claim was recorded. Live branches: `Your questions have been answered: \"<q>\"=\"<a>\". You can now continue with these answers in mind.` (100 carriers on disk), `The user answered: \"<q>\"=\"<a>\". Read the answers carefully - they may request clarification, changes, or that you not proceed - and follow what they actually say.` (16 carriers), and `The user did not answer the questions.` for the unanswered branch (0 carriers here). The historical `User has answered your questions: ...` phrasing is absent from the 2.1.258 binary and has zero live carriers in the current corpus, surviving only as quoted text inside other records.",
"depends": "csift's AUQ_ANSWER_MARKERS carries all three shipped phrasings since v0.10.1 (`User has answered your questions`, `Your questions have been answered`, `The user answered:`) and the search prefilter's verifiable synth-marker set carries the same three, so a marker-only carrier under any of them opens a turn and classifies user.answer; the unanswered branch (`The user did not answer the questions.`) is deliberately excluded so it never opens one. The START-anchored trim_start + starts_with design remains necessary - the retired phrasing draws 313 quotation matches corpus-wide that a contains check would false-positive on.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "3-9",
"snippet": "/// The synthesized prefixes Claude Code writes into the `tool_result` answering an\n/// `AskUserQuestion` (§4.4). CC has shipped THREE phrasings for the same synthesized\n/// answer - verified across real `~/.claude/projects` data and the 2.1.258 binary:\n///\n/// - `\"User has answered your questions: \\\"<q>\\\"=\\\"<a>\\\". …\"`\n/// - `\"Your questions have been answered: \\\"<q>\\\"=\\\"<a>\\\". …\"` (the dominant form\n/// in older data; a single hardcoded marker missed it entirely)"
},
{
"path": "src/model/markers.rs",
"lines": "15",
"snippet": "pub const AUQ_ANSWER_MARKERS: &[&str] = &["
},
{
"path": "src/model/markers.rs",
"lines": "35-38",
"snippet": "pub fn is_auq_answer_text(text: &str) -> bool {\n let head = text.trim_start();\n AUQ_ANSWER_MARKERS.iter().any(|m| head.starts_with(m))\n}"
},
{
"path": "src/model/markers.rs",
"lines": "15-22",
"snippet": "pub const AUQ_ANSWER_MARKERS: &[&str] = &[\n \"User has answered your questions\",\n \"Your questions have been answered\",\n // CC 2.1.258's third branch (`The user answered: \"<q>\"=\"<a>\". Read the answers\n // carefully ...`); the fourth branch `The user did not answer the questions.` is\n // the UNANSWERED case and must never open a turn, so it stays out.\n \"The user answered:\",\n];"
},
{
"path": "src/model/markers.rs",
"lines": "34-38",
"snippet": "#[must_use]\npub fn is_auq_answer_text(text: &str) -> bool {\n let head = text.trim_start();\n AUQ_ANSWER_MARKERS.iter().any(|m| head.starts_with(m))\n}"
},
{
"path": "src/search/matcher.rs",
"lines": "487-495",
"snippet": " let mut verifiable: Vec<&[u8]> = vec![\n b\"<task-notification>\",\n br#\"\"answers\"\"#,\n b\"User has answered your questions\",\n b\"Your questions have been answered\",\n b\"The user answered:\",\n // The agents-stopped kill notice renders a fabricated `[subagent stopped]` head.\n b\"stopped by the user\",\n ];"
}
],
"instrument": "`rg -c 'User has answered your questions' ~/.claude/projects -g '*.jsonl'` versus `rg -c 'Your questions have been answered' ...` - both non-zero over a corpus spanning versions, a file carrying both spans a transition - then `csift search '' @<id> -t user.answer -c`. Counting rule: one per tool_result carrier whose content STARTS with the marker.",
"located": {
"claude_code": "2.1.258",
"csift": "0.1.0",
"source": "SPEC.md section 4.4; src/model/markers.rs AUQ_ANSWER_MARKERS doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'User has answered' ; (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}Your questions have been answered.{0,420}' ; (c) python3 over every top-level transcript: for each type=='user' record carrying a tool_result block, take the block's rendered content and count it when it STARTS WITH one of the candidate phrasings ; (d) csift search '<phrasing>' --count-by label for each phrasing",
"observed": "(a) 'User has answered' returns ZERO hits in the 2.1.258 binary - the older phrasing is no longer shipped. (b) the surviving site is a THREE-branch conditional, quoted verbatim from the binary: `Your questions have been answered: ${_}. You can now continue with these answers in mind.`:`The user answered: ${_}. Read the answers carefully \\u2014 they may request clarification, changes, or that you not proceed \\u2014 and follow what they actually say.`;else v=\"The user did not answer the questions.\" (c) on disk: 100 carriers START with 'Your questions have been answered', 16 START with 'The user answered: ', ZERO with 'User has answered your questions', ZERO with 'The user did not answer the questions'. (d) the pattern 'User has answered your questions' still matches 313 records corpus-wide but every one is a quotation inside prose, a tool result or an attachment - none is a carrier and none keys under user.answer.",
"rule": "One per tool_result carrier whose rendered content STARTS with the phrasing (trim_start then starts_with, matching the code's own test); a mid-content quotation does not count, which is what separates the 313 pattern matches from the 0 real carriers.",
"note": "This is a genuine behavioural drift, found in the binary and then confirmed on disk. Claude Code has rotated the phrasing set: one of the two recorded markers is retired, and a third has shipped that csift does not know. The live blast radius is currently nil because the marker test is only the FALLBACK - all 16 `The user answered:` carriers also carry the structured `toolUseResult.answers` object, so `is_auq_answer_boundary` still opens their turns and they still classify `user.answer` (see TURN-010). The exposure is latent and one-sided: any carrier that ever arrives WITHOUT `toolUseResult` under the new phrasing would silently stop opening a turn. Adding `The user answered:` to AUQ_ANSWER_MARKERS closes it; the fourth string `The user did not answer the questions.` should stay OUT, since that branch is the unanswered case and must not open a turn. Code sites src/model/markers.rs:3-9, 14-17 and 30-33 all match verbatim - the code is exactly as the claim describes it, which is precisely why the drift shows up as a missing marker rather than a wrong one."
}
]
},
{
"id": "TURN-010",
"area": "turn-boundary",
"behavior": "An ANSWERED AskUserQuestion carrier is a `type:\"user\"` record with a non-errored `tool_result` block AND a non-empty structured `toolUseResult.answers` object. Re-measured 2026-09-02 across 64 top-level transcripts: all 95 distinct answered carriers carry a non-empty `answers` object and none carries `is_error:true`, but only 79 of the 95 (83%) also carry a marker string csift recognises - the other 16 lead with Claude Code 2.1.258's newer `The user answered: ...` phrasing, which is not in `AUQ_ANSWER_MARKERS` (see TURN-009).",
"depends": "`is_auq_answer_boundary` opens a turn on the structured signal, with the marker string as the fallback for an older record that has no `toolUseResult`; the answer is the user's message, so the carrier is a turn opener classified `user.answer` (deduped from its `agent.tool.result` twin by the richest-view law). The structured-signal-first ordering is what keeps the 16 new-phrasing carriers working, so the primary/fallback split is doing real load-bearing work rather than being belt-and-braces.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "240-252",
"snippet": " /// True when this record is an ANSWERED AskUserQuestion carrier that should open a\n /// turn (§4.4 / §6.4): a `type:\"user\"` record carrying a `tool_result` block whose\n /// `is_error` is not true, AND it is a real answer - signalled by a non-empty\n /// `toolUseResult.answers` object (the clean, structured source) OR, as a fallback\n /// for an older record without `toolUseResult`, the synthesized AUQ-answer marker in\n /// the tool_result content. The answer is a genuine USER message (the user's\n /// selection + prose reasoning), so it is a turn boundary.\n ///\n /// A CANCELLED / rejected / validation-errored AUQ (no `answers`, `is_error:true`,\n /// or a `Cancelled…` / `<tool_use_error>…` body) is NOT a boundary - those carry no\n /// typed user message. Verified on real data: all 81 answered carriers have\n /// non-empty `toolUseResult.answers`, the marker string, and `is_error` false; the\n /// rejection/cancel carriers have none of the three."
},
{
"path": "src/model/predicates.rs",
"lines": "254-262",
"snippet": " pub fn is_auq_answer_boundary(&self) -> bool {\n if !self.is_type(\"user\") {\n return false;\n }\n // The carrier must ride on a non-errored tool_result block.\n let Some(blocks) = self.blocks() else {\n return false;\n };\n let mut has_non_errored_tool_result = false;"
}
],
"instrument": "`csift search '' @<id> -t user.answer --format json | jq -r .line` and check each line carries `\"answers\":{` with at least one key and no `\"is_error\":true`. Counting rule: AUQ tool_result carriers, split by the three signals.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md section 3.3 case 2; src/model/predicates.rs is_auq_answer_boundary doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' -t user.answer --no-subagents --max-count 0 --raw > /tmp/ua.jsonl ; then jq over that file for (i) records with a non-empty object at .toolUseResult.answers, (ii) records where any tool_result block has is_error true, (iii) the first 34 characters of the tool_result content ; cross-checked with csift search '' -t user.answer --no-subagents --count-by label and with the independent python carrier scan from TURN-009",
"observed": "95 distinct records by uuid (116 record-instances in the census; the 21-instance gap is clone-inherited duplicate uuids across forked transcripts, which is documented behaviour, not a discrepancy - and 100-79=21 on the phrasing split confirms the duplicates are all of the dominant phrasing). Of the 95 distinct carriers: 95 carry a non-empty `toolUseResult.answers` object (95/95), 0 carry `is_error:true` on any tool_result block (0/95), and the leading text splits 79 `Your questions have been answered:` against 16 `The user answered: \"...`. So two of the three signals are universal and the third is not: only 79 of 95 (83%) carry a marker string csift recognises.",
"rule": "One per distinct AUQ tool_result carrier record, keyed by uuid to collapse clone-inherited duplicates; the census's 116 counts record-instances instead. Split by the three signals independently: structured non-empty `.toolUseResult.answers`, absence of `is_error:true`, and a recognised leading marker string.",
"note": "The claim's structural core survives intact and was confirmed by sweeping every user.answer record in the corpus: 95/95 on the structured answers object, 0/95 on is_error. What no longer holds is the 'all three signals' sub-assertion - 16 of 95 carriers now carry only two, because Claude Code changed the phrasing out from under the marker list. That the boundary still fires for all of them is a direct vindication of preferring the structured signal over the string, and it is the reason TURN-009's drift is currently latent rather than live. The 81 -> 95 count change also carries a counting-rule correction: uuid-distinct carriers are 95 while raw record-instances are 116, the difference being clone-inherited copies. Code sites src/model/predicates.rs:240-252 and 254-262 both match verbatim."
}
]
},
{
"id": "TURN-011",
"area": "turn-boundary",
"behavior": "A cancelled, rejected or validation-errored AskUserQuestion lands as a tool_result with is_error:true and no toolUseResult object at all (so no answers map). Two body shapes are observed, not one: the generic tool-rejection text 'The user doesn't want to proceed with this tool use...' (9 of 15) and '<tool_use_error>InputValidationError...' (6 of 15); no 'Cancelled...' body occurs in the corpus. A THIRD non-answer shape exists that the claim does not mention and that is NOT an error: when the picker times out, Claude Code 2.1.258 writes an is_error-absent carrier whose body is 'No response after 60s - the user may be away' with an EMPTY toolUseResult.answers map (3 specimens). csift rejects it for the right reason anyway - the empty map fails has_auq_answers and the body matches no AUQ-answer marker - so it opens no turn and never labels user.answer.",
"depends": "`is_auq_answer_boundary` returns false on the FIRST errored `tool_result` block, so a cancelled question never opens a turn and never counts as `user.answer`; admitting the errored form fabricates a user turn out of an abandoned question.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "263-273",
"snippet": " for b in blocks {\n if let Block::ToolResult { is_error, .. } = b {\n if is_error.unwrap_or(false) {\n return false; // an errored AUQ result (cancel/reject) is never a boundary\n }\n has_non_errored_tool_result = true;\n }\n }\n if !has_non_errored_tool_result {\n return false;\n }"
}
],
"instrument": "`csift search '' @<id> --count-by result` splits tool results `ok` / `error`; then fetch an `error` AUQ carrier with `csift show @<id> --line <n>` and confirm it carries no `user.answer` label. Counting rule: one per AUQ tool_result carrier record.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md section 3.3 case 2; SPEC.md section 4.4; src/model/predicates.rs is_auq_answer_boundary body"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search 'AskUserQuestion' -t agent.tool.use --format json | jq -r '.hits[]?|select(.tool_name==\"AskUserQuestion\")|.tool_use_id' | sort -u > IDS ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | .message.content[]? | select(.type==\"tool_result\") | . as $b | [$b.tool_use_id, ($b.is_error // false), (($r.toolUseResult // null) | if type==\"object\" then ((.answers // null) | if type==\"object\" then (keys|length) else -1 end) else -2 end), ((if ($b.content|type)==\"string\" then $b.content else \"\" end)[0:42])] | @tsv' | grep -F -f IDS ; csift search 'InputValidationError' --count-by label ; csift search 'No response after 60s' --count-by label ; csift --claude-home $FX search LIMA --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "134 AUQ carriers joined by tool_use_id. is_error:true = 15, split 9 bodies opening 'The user doesn't want to proceed with this tool use' and 6 opening '<tool_use_error>InputValidationError'; all 15 carry NO toolUseResult object at all, so no answers map. is_error:false = 119, of which 116 carry a non-empty toolUseResult.answers map (1-4 keys) and 3 carry an EMPTY answers map with the body 'No response after 60s - the user may be away'. Label census: 'InputValidationError' -> 119 agent.tool.result, 0 user.answer; 'No response after 60s' -> 7 agent.tool.result, 0 user.answer. Fixture through csift 0.10.0 (four records, one genuine opener first): the is_error:true InputValidationError carrier and the is_error-absent empty-answers carrier both stay in turn 0 labelled agent.tool.result, while the non-empty-answers carrier opens turn 1 as user.answer.",
"rule": "One row per tool_result block whose tool_use_id joins to an AskUserQuestion tool_use block anywhere in the corpus (7,762 .jsonl files under ~/.claude/projects). In the answers column -2 means the record carries no toolUseResult object at all and 0 means the map is present but empty.",
"note": "Holds, with the body taxonomy corrected and one shape added. The claim's 'indistinguishable from an ordinary tool error' is exactly right for the 15 errored carriers: their bodies are the generic rejection text and a generic validation error, and nothing on the record names AskUserQuestion. csift's guard is stronger than the claim describes, because the empty-answers timeout carrier is is_error-absent yet still correctly refused. Code site src/model/predicates.rs 263-273 verified verbatim in the current file."
}
]
},
{
"id": "TURN-012",
"area": "turn-boundary",
"behavior": "Only RESOLVED AskUserQuestion invocations reach the transcript, not only answered ones. Claude Code 2.1.258 resolves the tool on four terminal branches and writes a tool_result for each: the two answered phrasings, an explicit 'The user did not answer the questions.', and an away/timeout path ('No response after 60s - the user may be away', 3 corpus specimens; the binary also carries 'Before going idle the user had selected: ${_}.'). So an abandoned picker can end up on disk as a harness-resolved result rather than as nothing at all.",
"depends": "csift therefore treats the ANSWERING carrier as the turn opener and renders the whole question-plus-options-plus-answer unit from it; a blocked-on-human lane can only be seen through the elicitation sidecar a hook writes, never through the native transcript.",
"code": [
{
"path": "src/elicitation.rs",
"lines": "4-7",
"snippet": "//! Three Claude Code elicitations stall a session on a human yet are invisible / ambiguous\n//! in the native jsonl while pending: **AskUserQuestion** and **ExitPlanMode** (CC buffers\n//! the whole assistant turn until answered - nothing on disk during the wait, see §3.4) and\n//! an **MCP Elicitation** (the inner request lives in memory). A Claude Code hook records"
},
{
"path": "src/model/predicates.rs",
"lines": "240-246",
"snippet": " /// True when this record is an ANSWERED AskUserQuestion carrier that should open a\n /// turn (§4.4 / §6.4): a `type:\"user\"` record carrying a `tool_result` block whose\n /// `is_error` is not true, AND it is a real answer - signalled by a non-empty\n /// `toolUseResult.answers` object (the clean, structured source) OR, as a fallback\n /// for an older record without `toolUseResult`, the synthesized AUQ-answer marker in\n /// the tool_result content. The answer is a genuine USER message (the user's\n /// selection + prose reasoning), so it is a turn boundary."
}
],
"instrument": "While an AskUserQuestion picker is open in a live session, `rg -c AskUserQuestion` over that session's own transcript returns only previously answered ones; counting rule: one per `tool_use` block naming the tool. Only a live session can show it.",
"located": {
"claude_code": "2.1.191",
"csift": "0.1.0",
"source": "SPEC.md section 4.4; AGENTS.md sections 3.4 and 3.10"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' -t agent.tool.use --count-by pairing ; csift search 'AskUserQuestion' -t agent.tool.use --format json | jq -r '.hits[]?|select(.tool_name==\"AskUserQuestion\")|.pairing' | sort | uniq -c ; csift search 'ExitPlanMode' -t agent.tool.use --format json | jq -r '.hits[]?|select(.tool_name==\"ExitPlanMode\")|.pairing' | sort | uniq -c ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Your questions have been answered'",
"observed": "Corpus-wide 210,896 tool_use records: 210,865 paired, 31 pending. Every one of the 31 pending is a Bash (4) or SendMessage (27) call. AskUserQuestion: 134 of 134 paired, 0 pending. ExitPlanMode: 16 of 16 paired, 0 pending. The 2.1.258 binary builds the AUQ tool_result in one place and returns a result on every branch: '...`Your questions have been answered: ${_}. You can now continue with these answers in mind.`:`The user answered: ${_}. Read the answers carefully ... and follow what they actually say.`;else v=\"The user did not answer the questions.\";return{type:\"tool_result\",content:v,tool_use_id:f}'. On-disk elicitation sidecars written by a hook: 20 elicitations.jsonl files carrying 124 pending AskUserQuestion and 12 pending ExitPlanMode markers; all 112 distinct sidecar pending keys later resolve to a native tool_use record.",
"rule": "One per tool_use block; pairing='pending' means no tool_result with that tool_use_id exists anywhere in scope. Interactive-elicitation tools are compared against the whole-corpus pending rate (31 of 210,896 = 0.015%) as the control.",
"note": "Holds on the pairing instrument, with the 'only answered ones appear' wording corrected to 'only resolved ones'. One residual: a post-hoc corpus cannot watch the transient state, so this is a differential (0 pending out of 150 interactive-elicitation tool_uses against 31 pending elsewhere), not a direct observation of an open picker. A timestamp probe was tried and is NOT evidence of buffering: joining the sidecar pending markers to their native records gives native_ts minus sidecar_pending_ts of n=112, median 0.4s, max 0.9s, i.e. the assistant record carries the GENERATION instant, so once written it reveals nothing about how long the human took. What would settle it: attach to a live session with an open AskUserQuestion picker and scan its own transcript for that tool_use id before answering. Code sites src/elicitation.rs 4-7 and src/model/predicates.rs 240-246 verified verbatim."
}
]
},
{
"id": "TURN-013",
"area": "turn-boundary",
"behavior": "Same two sub-shapes, current counts: 70 rejection carriers corpus-wide, 10 with the typed-instruction delimiter and 60 bare. The claim's 31 and 36 are stale measurements; the bare form now outnumbers the typed one 6 to 1.",
"depends": "Only the delimiter-bearing shape opens a turn (`is_plan_rejection_boundary`, classified `user.rejection`), because everything AFTER the delimiter is the genuine typed message; a bare rejection carries no typed message, so admitting it fabricates an empty human turn out of every rejected Edit, and concatenating the boilerplate as the operator's words corrupted 51 records. The delimiter is registered as a CONSERVATIVE synth-prefilter marker because the `[plan: <path>]` pointer its render resolves comes from a DIFFERENT record.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "129-134",
"snippet": "/// The synthesized marker Claude Code writes into the `tool_result` when the user\n/// REJECTS a tool use (§4.2.4) - fires for ANY rejected tool_use (ExitPlanMode plan\n/// kick-backs AND rejected AskUserQuestion / Edit / etc.). On its own it is NOT a user\n/// turn; it becomes one only when followed by the [`PLAN_REJECTION_USER_PREFIX`] tail\n/// (a real typed user instruction).\npub const PLAN_REJECTION_MARKER: &str = \"The user doesn't want to proceed with this tool use\";"
},
{
"path": "src/model/markers.rs",
"lines": "136-140",
"snippet": "/// The fixed ASCII delimiter that precedes the user's typed instruction in a\n/// rejection-with-message (§4.2.4): everything AFTER it is the genuine user message.\n/// A rejection WITHOUT this delimiter (the `STOP what you are doing and wait…` form)\n/// carries no typed message and must NOT open a turn.\npub const PLAN_REJECTION_USER_PREFIX: &str = \"To tell you how to proceed, the user said:\\n\";"
},
{
"path": "src/model/exchange.rs",
"lines": "193-199",
"snippet": " /// True when this record is a tool-use rejection carrying a typed user instruction\n /// (§4.2.4) and so should open a turn. A rejection without a typed message is NOT a\n /// boundary (see [`Record::plan_rejection_message`]).\n #[must_use]\n pub fn is_plan_rejection_boundary(&self) -> bool {\n self.plan_rejection_message().is_some()\n }"
},
{
"path": "src/search/matcher.rs",
"lines": "521-522",
"snippet": " // CONSERVATIVE (needs cross-record / external data - force the full scan).\n let mut conservative: Vec<&[u8]> = vec![b\"To tell you how to proceed\"];"
}
],
"instrument": "`rg -c \"The user doesn't want to proceed with this tool use\" ~/.claude/projects -g '*.jsonl'` for the total against `rg -c 'To tell you how to proceed, the user said' ...` for the typed subset (the second must be a strict subset; the difference is the bare form), then `csift search '' -t user.rejection -c`, which should equal the second count. Counting rule: one per record carrying the marker.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md section 3.3 case 3; SPEC.md section 4.2.4; SKILL.md wrong-assumption table"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg \"doesn't want to proceed with this tool use\" ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | .message.content[]? | select(.type==\"tool_result\") | (if (.content|type)==\"string\" then .content else ((.content // []) | map(.text // \"\") | join(\" \")) end) | select(startswith(\"The user doesn'\\''t want to proceed with this tool use\")) | if test(\"To tell you how to proceed, the user said:\\n\") then \"typed\" else \"bare\" end' | sort | uniq -c ; csift search '' -t user.rejection --count-by label ; csift --claude-home $FX search MIKE --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ; csift --claude-home $FX search FOXTROT --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "The binary carries both templates as adjacent literals: 'The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.' and the same sentence ending 'To tell you how to proceed, the user said:'. Corpus: 70 tool_result bodies START with the rejection marker, split 10 typed (carrying the delimiter) and 60 bare. csift reports exactly 10 user.rejection records corpus-wide - an exact match with the typed count. Fixture through csift 0.10.0: a typed-tail rejection opens its own turn as user.rejection; a bare 'STOP what you are doing' rejection stays inside the preceding turn as agent.tool.result.",
"rule": "One per tool_result block whose flattened text STARTS with the rejection marker (a mid-prose quotation does not count), split by whether the body contains the exact delimiter 'To tell you how to proceed, the user said:' followed by a newline. Corpus is 7,762 .jsonl files under ~/.claude/projects.",
"note": "Structure holds exactly and the delimiter is byte-identical in 2.1.258. Only the counts moved. The csift-side cross-check is unusually tight: the structural typed count (10) and the user.rejection label count (10) agree to the record. Code sites src/model/markers.rs 124-129 and 131-135, src/model/exchange.rs 193-199, src/search/matcher.rs 512-513 all verified verbatim."
}
]
},
{
"id": "TURN-014",
"area": "turn-boundary",
"behavior": "The plan APPROVAL carrier is structurally distinct from every rejection shape: it reads `User has approved your plan...`, carries no `is_error` and no typed message - it is the harness greenlight, not something the operator wrote.",
"depends": "csift never treats an approval as a user message or a turn boundary: it matches no rejection delimiter and, carrying no `answers` map and no AUQ marker, it is not an answered-AUQ boundary either, so `opens_turn` stays false and the human-turn count does not grow by one per approved plan.",
"code": [
{
"path": "src/model/tests/boundaries.rs",
"lines": "333-335",
"snippet": "fn plan_approval_is_not_a_boundary() {\n // The approval path is the harness greenlight (no typed message, no is_error) -\n // must NOT become a turn boundary."
},
{
"path": "src/model/exchange.rs",
"lines": "214-219",
"snippet": " pub fn opens_turn(&self) -> bool {\n self.is_genuine_user()\n || self.is_auq_answer_boundary()\n || self.is_plan_rejection_boundary()\n || self.is_peer_message_record()\n }"
}
],
"instrument": "`rg -c 'User has approved your plan' ~/.claude/projects -g '*.jsonl'` for the raw count, then confirm `csift search '' @<that session> -t user.message -c` does not grow by it. Counting rule: one per raw occurrence for rg, one per classified record for csift.",
"located": {
"claude_code": null,
"csift": null,
"source": "SPEC.md section 4.2.4; src/model/tests/boundaries.rs plan_approval_is_not_a_boundary"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'User has approved your plan' ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | .message.content[]? | select(.type==\"tool_result\") | . as $b | (if ($b.content|type)==\"string\" then $b.content else ((($b.content // []) | map(.text // \"\") | join(\" \"))) end) as $t | select($t | startswith(\"User has approved your plan\")) | [($b|has(\"is_error\")), (($r.toolUseResult // {}) | keys | join(\",\")), ($r.isMeta // false)] | @tsv' | sort | uniq -c ; csift search 'User has approved your plan' --count-by label ; csift --claude-home $FX search ECHO --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "Binary literal: 'User has approved your plan. You can now start coding. Start with updating your todo list if applicable'. Corpus: 12 approval carriers. All 12 have has(\"is_error\") false - the key is ABSENT, not false - and isMeta absent. toolUseResult keys are filePath,hasTaskTool,isAgent,plan on 7 and the same plus planWasEdited on 5; no answers key on any. csift label census over the approval text: 51 agent.tool.result, 15 agent.tool.use, 8 agent.thinking, 4 agent.message, 2 agent.communication.inbox and ZERO keys under any user.* leaf. Fixture through csift 0.10.0: an approval carrier placed after a genuine opener stays inside that turn (turn index unchanged) and labels agent.tool.result.",
"rule": "One per tool_result block whose flattened text starts with 'User has approved your plan' across 7,762 .jsonl files under ~/.claude/projects; the csift side counts one per classified record under every surviving leaf.",
"note": "Holds without correction. Worth recording as extra structure: the approval carrier is positively distinguishable from an answered AskUserQuestion not only by the missing answers map but by carrying a plan-shaped toolUseResult (filePath, plan, isAgent, hasTaskTool, optional planWasEdited) instead. Code sites src/model/tests/boundaries.rs 324-326 and src/model/exchange.rs 214-219 verified verbatim."
}
]
},
{
"id": "TURN-015",
"area": "turn-boundary",
"behavior": "An inbound peer message is still a type:user, role:user, STRING-content record excluded from is_genuine_user and kept as a turn opener, but its shape is no longer always the relay wrapper. Current corpus: 464 of 854 teammate-bearing records begin with the bare '<teammate-message' tag and carry no preamble; 390 begin with 'Another Claude session sent a message:'. Claude Code 2.1.258 additionally ships two preamble variants the claim does not list: 'Another Claude session sent a message while you were working:' and 'A peer session sent a message while you were working:'.",
"depends": "csift excludes peer messages from `is_genuine_user` while keeping them turn OPENERS via `is_peer_message_record`, so turn segmentation stays byte-stable where peers already opened turns while the `user` mislabel is removed; the record classifies `agent.communication.inbox` with a `teammate_id` to `self` direction, and its body renders through `inbound_comm_preview` / `record_text_sections`, never as raw XML.",
"code": [
{
"path": "src/model/peer.rs",
"lines": "45-52",
"snippet": "/// True when `content` is an inbound teammate/peer message (GOLD §5) - it carries a\n/// [`TEAMMATE_MESSAGE_OPEN`] tag at a section BOUNDARY (FINDING-1). Real data (edge-fixtures scout):\n/// the real shape is ALWAYS the relayed wrapper `Another Claude session sent a message:\\n\n/// <teammate-message …>\\n<BODY>\\n</teammate-message>\\n\\n<security footer>` (126 of 126), so the\n/// boundary is the content start, just after the relay preamble, or right after a prior section's\n/// close tag. A tag merely QUOTED mid-prose (a genuine user message that mentions the literal tag -\n/// common in csift's OWN dev sessions) is NOT a boundary, so the record stays `user.message` rather\n/// than being mislabeled `agent.communication.inbox` (the FINDING-1 fix)."
},
{
"path": "src/model/markers.rs",
"lines": "148",
"snippet": "pub const TEAMMATE_MESSAGE_OPEN: &str = \"<teammate-message\";"
},
{
"path": "src/model/markers.rs",
"lines": "151",
"snippet": "/// `Another Claude session sent a message:\\n<teammate-message …>`. A peer tag IMMEDIATELY after"
},
{
"path": "src/model/classify.rs",
"lines": "14-21",
"snippet": " /// True when this record is ANY inbound PEER message (GOLD §1 + FINDING-2) - a\n /// `<teammate-message>` OR `<agent-message>` at a section boundary. The predicate\n /// [`Record::is_genuine_user`] EXCLUDES and [`Record::opens_turn`] INCLUDES (a peer message is\n /// not the operator, but it still delimits a turn). Reads the raw (un-normalized) message text so\n /// the relay preamble's `\\n` survives; gated to `type:\"user\"` (the only place a peer message\n /// arrives). The body render for a peer-opened turn comes from\n /// [`Record::inbound_comm_preview`] (`turns`/`list`) / `record_text_sections` (`search`).\n #[must_use]"
}
],
"instrument": "`rg -c '<teammate-message' ~/.claude/projects/*/*.jsonl` and `rg -c 'Another Claude session sent a message:' ...` against `csift search '' @<id> --count-by label`, where the traffic must key under `agent.communication.inbox` and none of it under `user.message`. Counting rule: one per record carrying the open tag at a section boundary (csift's number is the lower, correct one).",
"located": {
"claude_code": "2.1.191",
"csift": "0.2.0",
"source": "AGENTS.md section 3.3; SPEC.md sections 4.1 and 5.3; src/model/peer.rs doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | (.message.content // null) as $c | select(($c|type)==\"string\") | select(($c | test(\"<teammate-message\")) or ($c | test(\"<agent-message\"))) | [(if ($c | startswith(\"Another Claude session sent a message:\")) then \"relay-preamble\" elif ($c | startswith(\"<teammate-message\")) then \"bare-teammate-start\" elif ($c | startswith(\"<agent-message\")) then \"bare-agent-start\" else \"other-position\" end), (if ($c | test(\"<teammate-message\")) then \"T\" else \"-\" end) + (if ($c | test(\"<agent-message\")) then \"A\" else \"-\" end), ($r.isMeta // false)] | @tsv' | sort | uniq -c ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Another Claude session sent a message' ; csift --claude-home $FX search NEEDLEBARE --count-by label ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "909 type:user string-content records carry a peer open tag. Of the 854 teammate-bearing ones, 464 START with the bare '<teammate-message' tag and 390 start with the relay preamble 'Another Claude session sent a message:' - so the preamble is present on 46%, not 100%. The 2.1.258 binary defines three preamble forms in one place: 'var QY=\"Another Claude session sent a message\",pe=`${QY} while you were working:`,me=`${QY}:`,gt=\"A peer session sent a message while you were working:\"'. Fixture through csift 0.10.0: a bare '<teammate-message teammate_id=...>' at content start labels agent.communication.inbox, so csift handles the preamble-less majority correctly.",
"rule": "One per type:user record whose message.content is a STRING containing a peer open tag, bucketed by what the content STARTS with, over 7,762 .jsonl files under ~/.claude/projects.",
"note": "Holds structurally; the '126 of 126 relay wrapper' quantifier is stale and should not be relied on. The same stale sentence is copied into the doc comment at src/model/peer.rs 45-52, which is worth correcting in place. csift's behaviour is unaffected for the bare and colon-preamble forms because content start is itself a section boundary; the two 'while you were working' variants are the real problem and are recorded under TURN-016. Code sites src/model/peer.rs 45-52, src/model/markers.rs 143 and 145-149, src/model/classify.rs 14-21 verified verbatim."
}
]
},
{
"id": "TURN-016",
"area": "turn-boundary",
"behavior": "The `<agent-message from=\"...\">` peer form is delivered as a type:\"user\", role:\"user\", STRING-content record and is isMeta-carrying (47 of 47 corpus records have isMeta:true). Claude Code 2.1.258 relays it under one of THREE preambles the binary defines together in one literal and strips as one list: `Another Claude session sent a message:`, the same head plus ` while you were working:`, and `A peer session sent a message while you were working:`. The mid-turn `while you were working` form carried 29 of those 47 records, so preamble recognition, not the tag alone, decides whether a relay is seen.",
"depends": "csift folds the form into `is_peer_message` on CONTENT shape rather than on `isMeta`, and anchors it at a section boundary that accepts ALL THREE relay preambles (`PEER_MESSAGE_PREAMBLES`, the same set the binary strips), so an isMeta agent-message still opens a turn and classifies `agent.communication.inbox` with the `from` attribute as the direction source. Keying peer detection off `isMeta` drops the form entirely; carrying only the colon preamble drops every mid-turn relay into the empty label vector - no turn, no leaf, no census row, no rendered `show` address, only `show --raw`.",
"code": [
{
"path": "src/model/peer.rs",
"lines": "59-62",
"snippet": "/// True when `content` is an inbound `<agent-message from=\"…\">` peer message (P1c M1 / FINDING-2) at\n/// a section BOUNDARY - the DISTINCT peer form from [`is_teammate_message`]. Like a teammate message\n/// it classifies `agent.communication.inbox`, is excluded from [`Record::is_genuine_user`], yet\n/// still opens a turn. Boundary-anchored (FINDING-1) for the same reason - a quoted tag is not it."
},
{
"path": "src/model/peer.rs",
"lines": "22-75",
"snippet": "#[must_use]\npub fn is_peer_message(content: &str) -> bool {\n is_teammate_message(content) || is_agent_message(content)\n}"
},
{
"path": "src/model/markers.rs",
"lines": "169",
"snippet": "pub const AGENT_MESSAGE_OPEN: &str = \"<agent-message\";"
},
{
"path": "src/model/markers.rs",
"lines": "158-162",
"snippet": "pub const PEER_MESSAGE_PREAMBLES: &[&str] = &[\n \"Another Claude session sent a message:\",\n \"Another Claude session sent a message while you were working:\",\n \"A peer session sent a message while you were working:\",\n];"
},
{
"path": "src/model/peer.rs",
"lines": "120-127",
"snippet": "pub(crate) fn is_section_boundary(prefix: &str) -> bool {\n let t = prefix.trim_end();\n t.is_empty()\n || PEER_MESSAGE_PREAMBLES.iter().any(|p| t.ends_with(p))\n || t.ends_with(TASK_NOTIFICATION_CLOSE)\n || t.ends_with(TEAMMATE_MESSAGE_CLOSE)\n || t.ends_with(AGENT_MESSAGE_CLOSE)\n}"
}
],
"instrument": "`rg -c '<agent-message ' ~/.claude/projects/*/*.jsonl`, then `csift search '<agent-message from=' --count-by label` and `--raw | jq -r .isMeta | sort | uniq -c`. Counting rule: one per record carrying the open tag at a section boundary.",
"located": {
"claude_code": "2.1.191",
"csift": "0.4.0",
"source": "AGENTS.md section 3.3; src/model/peer.rs is_agent_message doc comment; dev session 2026-09-01"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'def rtrim: sub(\"[[:space:]]+$\";\"\"); def isbnd($c;$open): ((($c | indices($open)) // []) | map(. as $i | ($c[0:$i] | rtrim) as $t | ($t==\"\") or ($t|endswith(\"Another Claude session sent a message:\")) or ($t|endswith(\"</task-notification>\")) or ($t|endswith(\"</teammate-message>\")) or ($t|endswith(\"</agent-message>\"))) | any); select(.type==\"user\") | . as $r | (.message.content // null) as $c | select(($c|type)==\"string\") | select(($c|test(\"<teammate-message\")) or ($c|test(\"<agent-message\"))) | [(if isbnd($c;\"<teammate-message\") or isbnd($c;\"<agent-message\") then \"boundary\" else \"quoted-only\" end), ($r.isMeta // false)] | @tsv' | sort | uniq -c ; csift search 'sent a message while you were working' --count-by label ; csift --claude-home $FX search NEEDLENEW --count-by label ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "47 type:user string-content records carry '<agent-message', all with isMeta:true. Re-running csift's own is_section_boundary rule in jq splits them 18 boundary / 29 quoted-only, and the 29 quoted-only ones are exactly the records whose preamble is 'Another Claude session sent a message while you were working:' - a form csift's PEER_MESSAGE_PREAMBLE does not contain. Consequence measured three ways: (1) csift search 'sent a message while you were working' --count-by label returns 6 records, all agent.tool.result / agent.thinking / agent.tool.use prose from same-day sessions, and ZERO under agent.communication.inbox; (2) on a live specimen csift show <target> --line N fails with 'no such record(s)' while csift show <target> --line N --raw prints the line, so the record is present on disk but absent from the rendered record set; (3) a controlled fixture through csift 0.10.0 with four peer records gives: old colon preamble -> 1 agent.communication.inbox, 'while you were working' preamble -> 0 records, 'A peer session sent a message while you were working:' -> 0 records, bare '<teammate-message' at content start -> 1 agent.communication.inbox.",
"rule": "One per type:user record whose message.content is a STRING containing '<agent-message', over 7,762 .jsonl files under ~/.claude/projects; boundary-ness computed by reimplementing csift's is_section_boundary (content start, or the prefix right-trimmed ending in the colon preamble or one of the three section close tags).",
"note": "Drifted, and this is the load-bearing finding of the batch. The fix is to widen PEER_MESSAGE_PREAMBLE from a single constant to the three forms Claude Code 2.1.258 defines together in one binary literal ('var QY=\"Another Claude session sent a message\",pe=`${QY} while you were working:`,me=`${QY}:`,gt=\"A peer session sent a message while you were working:\"'), matching on the QY prefix rather than the exact colon form. Note the silence is total rather than partial: an unrecognised isMeta peer record produces no label at all, so no census or -t selector discloses it. Code sites src/model/peer.rs 59-62 and 72-75 and src/model/markers.rs 156 verified verbatim; the defect is in the boundary predicate they depend on, not in these lines."
}
]
},
{
"id": "TURN-017",
"area": "turn-boundary",
"behavior": "The mechanism is exactly as described, but 'immediately after the relay preamble' should read 'immediately after THE colon-form relay preamble'. Claude Code 2.1.258 ships three relay preambles and csift's is_section_boundary lists only one of them, so the boundary set is complete for close tags and content start but incomplete for preambles (see TURN-016).",
"depends": "csift recognises a tag only at a SECTION BOUNDARY - the content start, immediately after the relay preamble, or right after a prior section's close tag - so a batched record still yields every section while a quoting message stays `user.message` instead of being reparented to `agent.communication.inbox` or a harness notification.",
"code": [
{
"path": "src/model/peer.rs",
"lines": "33-43",
"snippet": "pub(crate) fn has_boundary_section(content: &str, open: &str) -> bool {\n let mut idx = 0;\n while let Some(rel) = content[idx..].find(open) {\n let start = idx + rel;\n if is_section_boundary(&content[..start]) {\n return true;\n }\n idx = start + open.len();\n }\n false\n}"
},
{
"path": "src/model/peer.rs",
"lines": "120",
"snippet": "pub(crate) fn is_section_boundary(prefix: &str) -> bool {"
},
{
"path": "src/model/markers.rs",
"lines": "171-178",
"snippet": "/// Section CLOSE tags (FINDING-1). A peer / `<task-notification>` open tag that sits right after\n/// one of these (modulo whitespace) is at a section BOUNDARY ([`is_section_boundary`]), so a\n/// BATCHED record's later sections are still recognized - while a tag QUOTED mid-prose (a genuine\n/// user message that merely mentions the literal tag, common in csift's OWN dev sessions) is NOT a\n/// boundary and never starts a section. Kept beside their open-tag constants so the pair never drift.\npub(crate) const TASK_NOTIFICATION_CLOSE: &str = \"</task-notification>\";\npub(crate) const TEAMMATE_MESSAGE_CLOSE: &str = \"</teammate-message>\";\npub(crate) const AGENT_MESSAGE_CLOSE: &str = \"</agent-message>\";"
}
],
"instrument": "In a session whose prose quotes the tag, `csift search '<teammate-message' @<id> --count-by label`: the quoting records must count under `user.message` and the relayed ones under `agent.communication.inbox`, with no record in both. Counting rule: one section per boundary-anchored open tag; records carrying the literal tag split by whether the first occurrence sits at a boundary.",
"located": {
"claude_code": null,
"csift": "0.2.0",
"source": "AGENTS.md section 3.3 (FINDING-1); src/model/peer.rs is_section_boundary doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift --claude-home $FX2 search BATCHONE --count-by label ; csift --claude-home $FX2 search BATCHTWO --count-by label ; csift --claude-home $FX2 search BATCHQUOTE --count-by label ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | (.message.content // null) as $c | select(($c|type)==\"string\") | [(if ($c | test(\"</task-notification>[[:space:]]*<(task-notification|teammate-message|agent-message)\")) then \"batch-after-taskn\" else empty end), (if ($c | test(\"</teammate-message>[[:space:]]*<(task-notification|teammate-message|agent-message)\")) then \"batch-after-teammate\" else empty end)] | select(length>0) | .[]' | sort | uniq -c ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "Fixture through csift 0.10.0: one record holding a '<task-notification>...</task-notification>' section followed by a '<teammate-message ...>' section carries BOTH harness.notification.task and agent.communication.inbox, and a needle in either section finds the record under both leaves; a separate record whose prose merely quotes '<teammate-message teammate_id=\"x\">' mid-sentence labels user.message ONLY, with no inbox or notification leaf. Corpus: 125 records carry a section open tag immediately after a close tag - 123 after '</teammate-message>' and 2 after '</task-notification>' - so batching is real, not hypothetical. The boundary reimplementation over the 909 peer-tag records finds 8 non-isMeta records where no occurrence sits at a boundary, i.e. genuine messages that only quote the tag.",
"rule": "One section per boundary-anchored open tag; a record counts as batched when a regex finds a section open tag separated from a preceding close tag by whitespace only. Corpus is 7,762 .jsonl files under ~/.claude/projects.",
"note": "Holds on both halves that were testable: batched records do yield every section, and a quoting message does stay user.message. Refined only because the singular 'the relay preamble' is now an incomplete description of Claude Code's behaviour. Code sites src/model/peer.rs 33-43 and 120-127 and src/model/markers.rs 158-165 verified verbatim."
}
]
},
{
"id": "TURN-018",
"area": "turn-boundary",
"behavior": "Claude Code injects automation completions as a `type:\"user\"`, non-`isMeta`, STRING-content record wrapped in `<task-notification>...</task-notification>`, carrying the inner tags `<task-id>`, `<status>`, `<summary>`, `<event>`, `<tool-use-id>` and optionally `<output-file>` / `<result>`. It passes every genuine-user gate, so it DOES open a turn.",
"depends": "csift keeps it as a turn opener (a real delivered trigger) but reparents its label to `harness.notification.<kind>` and renders `[<kind> <task-id> <status>] <summary>` via `automation_label`, so machine pulses are neither counted as operator prose nor dumped as raw XML into a reconstructed turn.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "120-127",
"snippet": "/// Prefix of a `<task-notification>…</task-notification>` user record - a MACHINE-INJECTED\n/// automation trigger (a background-command / workflow / spawned-task completion notice CC\n/// inserts as a `type:\"user\"`, non-`isMeta`, STRING-content record). It LOOKS like a human\n/// turn to [`Record::is_genuine_user`] (it passes every gate), so it DOES open a turn - but\n/// it is an automation pulse, not the operator's prose. [`Record::automation_trigger`]\n/// classifies it so surfaces can LABEL the segment (`[workflow <id> completed] <summary>`)\n/// instead of dumping the raw `<task-id>`/`<output-file>`/`<status>` XML wrapper.\npub const TASK_NOTIFICATION_PREFIX: &str = \"<task-notification>\";"
},
{
"path": "src/model/predicates.rs",
"lines": "133-141",
"snippet": " /// Classify this record as a MACHINE-INJECTED automation trigger, if it is one.\n ///\n /// A `<task-notification>` record is a `type:\"user\"`, non-`isMeta`, STRING-content\n /// record CC inserts when a background command / spawned task / workflow completes. It\n /// passes every [`Record::is_genuine_user`] gate (so it opens a turn like a human\n /// message), but it is an automation pulse - surfacing its raw `<task-id>` /\n /// `<output-file>` / `<status>` XML as \"user prose\" is noise. This parser extracts the\n /// stable inner tags so a surface can render `[workflow <task-id> completed] <summary>`\n /// instead. Returns `None` for any non-`<task-notification>` record."
}
],
"instrument": "`rg -c '<task-notification>' <transcript>` against `csift search '' @<id> --count-by label | rg harness.notification`. Counting rule: one per boundary-anchored notification SECTION (a batched record yields several) for csift, one per line for rg.",
"located": {
"claude_code": null,
"csift": "0.4.0",
"source": "AGENTS.md section 3.3; SPEC.md section 5.1; src/model/markers.rs TASK_NOTIFICATION_PREFIX doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | (.message.content // null) as $c | select(($c|type)==\"string\") | select($c | startswith(\"<task-notification>\")) | [($r.isMeta // false), ($c|test(\"<task-id>\")), ($c|test(\"<status>\")), ($c|test(\"<summary>\")), ($c|test(\"<event>\")), ($c|test(\"<tool-use-id>\")), ($c|test(\"<output-file>\")), ($c|test(\"<result>\"))] | @tsv' | sort | uniq -c ; csift search '' -t harness.notification --count-by label ; csift --claude-home $FX3 search CHARLIE --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "2,799 records whose type:user STRING content starts with '<task-notification>'. isMeta is false on all 2,799. Inner tags: <task-id> and <summary> on all 2,799; <status> on 1,962 and <event> on 837, and those two are mutually exclusive; <tool-use-id> and <output-file> on 1,957 each; <result> on 325. csift label census: 2,811 records across harness.notification.background-command 1,518, monitor 948, subagent 198, workflow 142, task 5. Fixture through csift 0.10.0: a task-notification record placed after a genuine opener and a compaction summary advances the turn index from 0 to 1 and labels harness.notification.task, so it does open a turn and is not counted as operator prose.",
"rule": "One per type:user record whose message.content is a STRING starting with '<task-notification>', over 7,762 .jsonl files under ~/.claude/projects; the csift side counts one per classified record per surviving leaf, which is why the notification total slightly exceeds the raw count (the agents-stopped notices of TURN-019 also key under harness.notification.subagent).",
"note": "Holds without correction, including the optional tags. Two details worth pinning for a future reader: <status> and <event> never co-occur (a monitor or scheduled pulse carries <event> and no <status>, which is why the renderer falls back to the event), and <result> appears on 325 of 2,799, matching the background-agent report shape that also earns agent.communication.inbox. Code sites src/model/markers.rs 115-122 and src/model/predicates.rs 133-141 verified verbatim."
}
]
},
{
"id": "TURN-019",
"area": "turn-boundary",
"behavior": "Same two templates, current count 10 specimens rather than 9. One dead branch to record: Claude Code 2.1.258 selects the plural template whenever the stopped count is not exactly 1, so the singular-COUNT phrasing '1 background agent was stopped by the user' is never emitted; csift's is_agents_stopped_notice accepts it anyway, which is harmless tolerance rather than a needed arm. The quoted items are the agents' descriptions, each an ellipsis-truncated prompt prefix averaging 54-55 characters.",
"depends": "Because it is a plain non-`isMeta` string that matches no other marker, it reads as a genuine human turn unless explicitly excluded: csift treats it as a synthetic user marker (never genuine, never a turn opener), classifies it `harness.notification.subagent` and renders `[subagent stopped] ...`; the dual-arm predicate is required because the singular template has no leading count. 9 corpus specimens were previously read as `user.message`.",
"code": [
{
"path": "src/model/markers.rs",
"lines": "54-58",
"snippet": "/// The harness's \"N background agent(s) were stopped by the user: ...\" notice (v0.10.0):\n/// a plain-string `type:\"user\"` record Claude Code writes when async agents are killed\n/// from the UI. It names a count and truncated prompt prefixes, never an id, and it\n/// triggers no generation - the model sees it only alongside the next real prompt. Not\n/// the operator, never a turn opener; classifies `harness.notification.subagent`."
},
{
"path": "src/model/markers.rs",
"lines": "60-75",
"snippet": "pub fn is_agents_stopped_notice(content: &str) -> bool {\n let s = content.trim_start();\n // The singular template names the agent: `Background agent \"<desc>\" was stopped by\n // the user.` (no count).\n if s.starts_with(\"Background agent \\\"\") && s.contains(\" was stopped by the user\") {\n return true;\n }\n let digits = s.bytes().take_while(u8::is_ascii_digit).count();\n if digits == 0 {\n return false;\n }\n let rest = &s[digits..];\n (rest.starts_with(\" background agent was stopped by the user\")\n || rest.starts_with(\" background agents were stopped by the user\"))\n && rest.contains(AGENTS_STOPPED_MARKER)\n}"
},
{
"path": "src/model/markers.rs",
"lines": "47-52",
"snippet": "pub fn is_synthetic_user_marker(content: &str) -> bool {\n INTERRUPT_MARKERS.contains(&content)\n || content.starts_with(LOCAL_COMMAND_STDOUT_PREFIX)\n || is_slash_command_wrapper(content)\n || is_agents_stopped_notice(content)\n}"
}
],
"instrument": "`rg -oNI 'background agents? (was|were) stopped by the user' ~/.claude/projects -g '*.jsonl' | sort | uniq -c` plus the singular `Background agent \"` form (both templates must be present for the dual arm to stay necessary), then `csift search 'stopped by the user' --count-by label`, where every hit keys under `harness.notification.subagent` and none under `user.message`. Counting rule: one per raw match for rg, one per classified record for csift.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.0",
"source": "AGENTS.md section 3.3a; SPEC.md section 5.1 and the v0.10.0 ledger; src/model/markers.rs is_agents_stopped_notice doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'stopped by the user' ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | (.message.content // null) as $c | select(($c|type)==\"string\") | select(($c|test(\"^Background agent \\\"\")) or ($c|test(\"^[0-9]+ background agents? (was|were) stopped by the user\"))) | [($r.isMeta // false), (if ($c|test(\"^Background agent \\\"\")) then \"singular\" else \"plural\" end), ($c | gsub(\"\\\"[^\\\"]*\\\"\";\"\\\"<desc>\\\"\") | .[0:90])] | @tsv' | sort | uniq -c ; csift search 'stopped by the user' --count-by label ; csift --claude-home $FX3 search DELTA --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "The 2.1.258 binary picks the template by count in one expression: 'let Fo=kn.length===1?`Background agent \"${kn[0]}\" was stopped by the user.`:`${kn.length} background agents were stopped by the user: ${kn.map((jn)=>`\"${jn}\"`).join(\", \")}.`;return Ye.enqueuePendingNotification({agentId:Ze(),value:Fo,mode:\"task-notification\",skipAttachments:!0})' - the value is a bare string, not the structured notification payload the other call sites pass. Corpus: 10 specimens, 1 singular and 9 plural with counts 2, 3, 5, 10 and 26; isMeta absent on all 10; the quoted item count always equals the leading number; every specimen contains '...' and the quoted descriptions average 54-55 characters. csift label census over 'stopped by the user': 10 harness.notification.subagent and ZERO under any user.* leaf. Fixture through csift 0.10.0: a plural notice placed mid-turn does NOT advance the turn index and labels harness.notification.subagent.",
"rule": "One per type:user record whose STRING content matches '^Background agent \"' or '^[0-9]+ background agents? (was|were) stopped by the user', over 7,762 .jsonl files under ~/.claude/projects.",
"note": "Holds; only the specimen count and one never-taken predicate arm needed correction. The bare-string value is what makes the exclusion necessary: unlike every other task-notification call site, this one enqueues raw prose with no XML wrapper and no isMeta, so without the explicit marker it would pass every genuine-user gate. Code sites src/model/markers.rs 49-53, 55-70 and 42-47 verified verbatim."
}
]
},
{
"id": "TURN-020",
"area": "turn-boundary",
"behavior": "A compaction SUMMARY is a `type:\"user\"` record carrying `isCompactSummary:true` with STRING content - not a `type:\"summary\"` record - so on the `type` field alone it looks like a human turn.",
"depends": "csift gates `isCompactSummary` out of `is_genuine_user` before any content inspection, so a summary never opens a turn and its machine recap never enters the operator census; it stays a turn MEMBER classified `harness.compaction.summary`, which is what lets `verbatim` walk backwards THROUGH a compaction rather than stopping at it.",
"code": [
{
"path": "src/model/record.rs",
"lines": "66-68",
"snippet": " /// Compaction summary marker - when true, this user record is NOT a human turn.\n #[serde(default, rename = \"isCompactSummary\")]\n pub is_compact_summary: Option<bool>,"
},
{
"path": "src/model/predicates.rs",
"lines": "37-43",
"snippet": " pub fn is_genuine_user(&self) -> bool {\n if !self.is_type(\"user\") {\n return false;\n }\n if self.is_compact_summary.unwrap_or(false) {\n return false;\n }"
}
],
"instrument": "`rg -c '\"isCompactSummary\":true' <transcript>` against `csift stats @<id> --format json | jq .compactions`, and confirm `csift search '' @<id> --count-by label` keys them under `harness.compaction.summary`, never `user.message`. Counting rule: one per record carrying the flag.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md sections 3.3 and 3.5; SPEC.md section 4.7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.isCompactSummary == true) | [.type, (.message.role // \"-\"), ((.message.content // null)|type), (.isMeta // false), (.isVisibleInTranscriptOnly // false)] | @tsv' | sort | uniq -c ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"summary\") | .type' | sort | uniq -c ; csift search '' -t harness.compaction --count-by label ; csift --claude-home $FX3 search BRAVO --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
"observed": "232 records carry isCompactSummary:true. All 232 are type:user with message.role user and STRING content, isMeta absent, isVisibleInTranscriptOnly true - a single uniform row, no variants. Zero records of type 'summary' exist anywhere in the corpus. csift label census: 232 harness.compaction.summary and 232 harness.compaction.boundary, a one-to-one pairing. Fixture through csift 0.10.0: a compaction summary placed after a genuine opener keeps turn index 0 - it does not open a turn - and labels harness.compaction.summary only, with no user.message leaf.",
"rule": "One per record carrying isCompactSummary:true, over 7,762 .jsonl files under ~/.claude/projects; the type:'summary' probe counts one per record of that type and returns nothing.",
"note": "Holds without correction, and the negative half of the claim is now measured rather than assumed: there is not one type:'summary' record in the corpus, so a reader keying on .type alone really does see only 'user'. The 232 summaries pair exactly one-to-one with 232 compact_boundary system records, which is a useful invariant for anyone auditing compaction points. Code sites src/model/record.rs 65-67 and src/model/predicates.rs 37-43 verified verbatim."
}
]
},
{
"id": "TURN-021",
"area": "turn-boundary",
"behavior": "A subagent transcript's FIRST record is an `isSidechain:true` `type:\"user\"` seed carrying the spawn prompt for a Task/Agent/teammate child; a `/fork` child leads with a `type:\"fork-context-ref\"` line (agentId, parentSessionId, parentLastUuid, contextLength) and its seed follows. No top-level transcript starts with a sidechain seed.",
"depends": "csift deliberately does NOT gate `isSidechain` out of `is_genuine_user`, so `list`'s per-subagent preview can treat that seed as the subagent's first user message (gating it would silently blank every subagent preview for zero real benefit); the seed classifies `agent.communication.inbox` with a parent-to-self direction, and the per-surface scan owns the subagent-versus-parent context instead.",
"code": [
{
"path": "src/model/predicates.rs",
"lines": "48-53",
"snippet": " // NOTE on `isSidechain`: a subagent transcript's FIRST record is an\n // `isSidechain:true` user seed. It is NOT gated out here on purpose - `list`'s\n // per-subagent preview legitimately treats that seed as the subagent's \"first\n // user message\", and in TOP-LEVEL transcripts a sidechain seed does not occur in\n // any real corpus. Gating it would silently blank the subagent preview for zero\n // real benefit; the per-surface scan owns subagent-vs-parent context instead."
}
],
"instrument": "`head -1 <a subagent transcript> | jq -r '[.isSidechain, .message.role] | @tsv'` must print `true` then `user`, `rg -c '\"isSidechain\":true' <a top-level transcript>` must be 0, and `csift list @<agent-id> --format json | jq -r .first_user` must be non-empty. Counting rule: one seed per subagent transcript.",
"located": {
"claude_code": null,
"csift": "0.1.0",
"source": "AGENTS.md section 3.3; SPEC.md section 1; src/model/predicates.rs isSidechain note"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "for each of three subagent lanes of one live session: csift show @<agent-id> --line 1 --raw | jq '{type,isSidechain,role:.message.role,agentId}'; for every top-level transcript: head -c 400 <file> | rg -q '\"isSidechain\":true' (count the files whose FIRST line carries the seed)",
"observed": "The three sampled lanes were /fork children and their first line is a `type:\"fork-context-ref\"` record (agentId set, no isSidechain, no message), so the sidechain seed is the SECOND record on a fork child; 0 of 67 top-level transcripts start with an isSidechain:true seed.",
"rule": "Line 1 of each transcript file only; a top-level file counts once regardless of later sidechain records.",
"note": "Refined for the fork shape; the top-level half holds exactly (0 of 67)."
}
]
},
{
"id": "TURN-022",
"area": "turn-boundary",
"behavior": "When the user recalls a sent message with escape, edits it and re-sends, Claude Code leaves the ORIGINAL on disk: the draft and the delivered resend are separate `type:\"user\"` records sharing ONE non-null `parentUuid`, and only the last in file order was delivered. The shared parent is usually a turn-end anchor record - measured parents were a `stop_hook_summary` system record 133 times, `turn_duration` 83, `away_summary` 80, an interrupt marker 19 and an assistant record once.",
"depends": "`superseded_draft_indices` keeps the LAST opener per `parentUuid` and marks the earlier siblings superseded, and `group_turn_indices_deduped` drops them entirely from turn numbering, so an edited-away message can never resurface as a phantom turn or leak its abandoned text into a neighbour. The signal is parent-uuid IDENTITY, not text similarity, which would miss the common prepend/insert edit; distinct real turns never share a parentUuid, and a record with a null/empty parent is never grouped.",
"code": [
{
"path": "src/model/grouping.rs",
"lines": "88-93",
"snippet": "/// Indices of turn-opening records that are SUPERSEDED DRAFTS - an earlier sibling of a\n/// later turn-opener sharing the SAME non-null `parentUuid` (§6.4.1). This is the on-disk\n/// shape of the \"type a message, ESC-cancel / edit, resend\" loop (and any rewind that\n/// re-opens a turn from the same point): Claude Code appends every draft as its own\n/// `type:\"user\"` record, yet only ONE - the last in file order - was actually delivered to\n/// the model. The earlier siblings are abandoned drafts."
},
{
"path": "src/model/grouping.rs",
"lines": "95-101",
"snippet": "/// WHY last-in-file is the survivor (verified on real `~/.claude/projects` data): distinct\n/// real turns never share a `parentUuid` (each user turn is parented to the assistant\n/// message that preceded it), so same-parent openers are ALWAYS alternative versions of one\n/// logical turn; and across the corpus the last sibling's subtree is the one that reaches\n/// furthest toward the leaf (the live branch). A content-similarity heuristic would miss the\n/// common case where the user *prepended/inserted* text on the edit (`look…` → `take a closer look…`),\n/// so the parent-uuid identity - not text - is the load-bearing signal."
},
{
"path": "src/model/grouping.rs",
"lines": "114-117",
"snippet": "pub fn superseded_draft_indices<T>(\n records: &[T],\n rec: impl Fn(&T) -> &Record,\n) -> std::collections::HashSet<usize> {"
},
{
"path": "src/model/grouping.rs",
"lines": "131-135",
"snippet": " // Keep the LAST opener per parent: when a new sibling appears, the previously-seen\n // one for that parent becomes a superseded draft.\n if let Some(prev) = latest.insert(parent, i) {\n superseded.insert(prev);\n }"
},
{
"path": "src/model/grouping.rs",
"lines": "140-153",
"snippet": "/// [`group_turn_indices`] with esc-cancel / edit-resend DRAFT SUPPRESSION (§6.4.1): a\n/// superseded draft ([`superseded_draft_indices`]) is dropped ENTIRELY - it neither opens a\n/// turn nor folds in as a member - so a message the user edited away before sending can\n/// never resurface as a phantom turn (nor leak its abandoned text into a neighbour). This is\n/// the delimiter every session-operating surface (`turns` / `search` / `files` / `recover`)\n/// uses, so they stay byte-consistent on what counts as a turn.\n#[must_use]\npub fn group_turn_indices_deduped<T>(\n records: &[T],\n rec: impl Fn(&T) -> &Record,\n) -> Vec<Vec<usize>> {\n let skip = superseded_draft_indices(records, |x| rec(x));\n group_turn_indices_core(records, |x| rec(x).opens_turn(), &skip)\n}"
}
],
"instrument": "Group all turn-opening user records of one transcript by `parentUuid` and count groups of size greater than one (counting rule: drafts = group size minus one, one per group), then cross-validate against csift's own `N superseded draft(s)` footer. Measured draft rates over four sessions: 11% (62 genuine / 7 drafts), 26% (1,216 / 317), 6% (86 / 5), 3% (32 / 1).",
"located": {
"claude_code": "2.1.252",
"csift": "0.8.2",
"source": "AGENTS.md section 3.3; SPEC.md section 6.4.1; src/model/grouping.rs doc comment; dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "csift search '' @<session> --no-subagents -t user.unsent --format json | jq 'select(.superseded_draft==true) | .hits[0].line' (first draft line), then sed -n '<line>p' <transcript> | jq -r .parentUuid and rg -c '\"parentUuid\":\"<that uuid>\"' <transcript>",
"observed": "The first superseded draft of the live session (L7292) has parentUuid facd56a1... and exactly 2 records share that parentUuid: the draft and its delivered resend; the draft renders with superseded_draft:true and turn_index null.",
"rule": "One draft record followed to its parent; sibling count = raw lines carrying the identical parentUuid value.",
"note": "The draft/resend pair sharing one parentUuid is the C-18 shape csift labels user.unsent."
}
]
},
{
"id": "TURN-023",
"area": "turn-boundary",
"behavior": "Draft groups are n-way, not pairs, and their content shape varies: in one 317-draft sample 230 drafts carried STRING content and 87 carried text-BLOCK content, and up to 8 drafts shared a single `parentUuid` (group sizes reaching 9).",
"depends": "csift's draft rule handles both content shapes and n-way groups rather than assuming a pair, and assigns the SINGLE leaf `user.unsent` at the SCAN layer - a pure per-record classify cannot see the LATER same-parent sibling that makes a record a draft - so `user.message` counts stay pure while the draft is still searchable, censusable, suppressed under a `--turn` window and always reachable by `show --line` / `--uuid` (JSON `superseded_draft:true` with a null `turn_index`).",
"code": [
{
"path": "src/search/hits.rs",
"lines": "172-176",
"snippet": " let labels = if superseded {\n vec![Class::UserUnsent]\n } else {\n rec.classify(ctx)\n };"
},
{
"path": "src/model/taxonomy.rs",
"lines": "44-51",
"snippet": " /// `user.unsent` - a SUPERSEDED turn-opener draft: sent, esc-recalled into the input\n /// box, edited and re-sent, leaving the original on disk sharing the resend's\n /// parentUuid. Assigned at the SCAN layer (the superseded set needs a LATER sibling,\n /// which a pure per-record classify cannot see); outside turn numbering; 99% never\n /// drew a reply. A recalled-then-ABANDONED message has no sibling and is\n /// structurally undetectable; a QUEUED text edited before dispatch never becomes a\n /// user record at all (it survives only in `queue-operation` lines).\n UserUnsent,"
}
],
"instrument": "`csift search '' @<id> -t user.unsent --format json | jq -r '[.label, .turn_index] | @tsv'` - every row must read `user.unsent` with a null turn index - and record for each draft whether `message.content` is a JSON string or an array. Counting rule: one per superseded opener record, identified by a shared `parentUuid` with a later opener in the same file.",
"located": {
"claude_code": "2.1.258",
"csift": "0.9.2",
"source": "AGENTS.md section 3.3; SPEC.md section 5.1; dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' <project-dir> -t user.unsent --count-by label (draft count) and sed -n '<draft line>p' <transcript> | jq -r '.message.content|type' (content shape of a sampled draft)",
"observed": "28 user.unsent records across the csift project dir; the sampled draft (L7292 of the live session) carries block-array content (an `array`), i.e. the text-block shape the claim lists beside the string shape.",
"rule": "Draft count = records the scan-layer superseded-draft rule labels user.unsent over the project's top-level and subagent lanes; shape read off one sampled draft.",
"note": "The 317-draft string-vs-block split (230/87, up to 8 per parent) is the 2026-09-01 measurement on another session and was not re-counted here; this check re-verified that both shapes occur (one block-shaped specimen) and the mechanism."
}
]
},
{
"id": "TURN-024",
"area": "turn-boundary",
"behavior": "Superseded drafts are overwhelmingly abandoned and are not in the surviving conversation: 99.1% never drew a reply (3 of 317 measured did), and Claude Code's own `compactMetadata.preservedMessages` accounting excludes every draft uuid (0 of 772 measured) while the conversation DAG threads through the resend sibling, never the draft.",
"depends": "That exclusion is the measured instrument behind classifying `user.unsent` LLM-invisible, and it is also the wording law: csift says 'not in the surviving conversation', never 'the model never saw it', because a few drafts did draw real replies before the retraction. csift also refuses to use `preservedMessages` membership as a general visibility test.",
"code": [
{
"path": "src/model/taxonomy.rs",
"lines": "160-165",
"snippet": " /// - `user.unsent`: a superseded draft is NOT in the surviving conversation -\n /// Claude Code's own `compactMetadata.preservedMessages` accounting excludes\n /// every draft uuid (0 of 772 measured), and the conversation DAG threads\n /// through the resend sibling, never the draft. (Wording law: \"not in the\n /// surviving conversation\", never \"the model never saw it\" - a few drafts\n /// drew real replies before the retraction.)"
}
],
"instrument": "Take the draft uuids from `csift search '' @<id> -t user.unsent --format json` and test membership in the compaction boundary's `preservedMessages` array read via `csift show @<id> --uuid <boundary> --raw`. Counting rule: one membership test per draft uuid (expect 0 of N); reply rate = drafts with at least one assistant child over drafts tested.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.2",
"source": "SPEC.md sections 5 and 5.1; src/model/taxonomy.rs llm_visible doc comment"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "csift search '' @<session> --no-subagents -t user.unsent --format json | jq '{superseded_draft, turn_index}' per hit (a draft is outside turn numbering); the 99.1% never-replied ratio needs the per-draft reply scan over the 317-draft session (rg for the draft uuid as a later parentUuid)",
"observed": "Every draft hit in the live session carries superseded_draft:true and turn_index:null (3 of 3 sampled), i.e. outside the numbered conversation as the claim states; the never-replied ratio was not re-measured in this audit.",
"rule": "Sampled hits of one session; the ratio stands as the 2026-09-01 measurement (3 of 317 drew a reply).",
"note": "Mechanism re-verified; the percentage is carried forward from the prior measurement and marked as such."
}
]
},
{
"id": "TURN-025",
"area": "turn-boundary",
"behavior": "Two neighbouring shapes leave NO recoverable record. A message esc-recalled and never resent has no later same-parent sibling, so it is structurally undetectable. A queued text edited before dispatch never becomes a user record at all - it survives only in `queue-operation` lines: over human prose enqueued into the queue (n=782, enqueue lines only, whitespace-normalized text join) 72% match a later user record exactly and 81% match by prefix, so roughly 19-28% of typed queue text never becomes a user record.",
"depends": "csift's `user.unsent` covers only SUPERSEDED drafts, never 'every unsent message'; the queue-only class is a separate, larger set reachable solely under the gated `user.queued` leaf, and csift documents the join rule and its counting basis instead of asserting a bare dispatched flag (no join key exists on the queue line).",
"code": [
{
"path": "src/model/grouping.rs",
"lines": "120-130",
"snippet": " for (i, item) in records.iter().enumerate() {\n let r = rec(item);\n if !r.opens_turn() {\n continue;\n }\n let Some(parent) = r.parent_uuid.as_deref() else {\n continue; // null parent: never grouped (would merge unrelated records)\n };\n if parent.is_empty() {\n continue;\n }"
}
],
"instrument": "Extract every `queue-operation` enqueue `content` and test whether the whitespace-normalized text appears as a later `type:\"user\"` record's string content or `text` block - `csift search '' @<id> -t user.queued --format json` against `-t user.message`. Counting rule: enqueue lines only, human prose only, one comparison per enqueued text; 'exact' = full-string equality, 'prefix' = the first 200 characters as a substring. An earlier figure of about 61% never-dispatched (54 of 88 texts, three-session sample) was measured over ALL queue operations and is superseded by the human-prose-only figures.",
"located": {
"claude_code": "2.1.252",
"csift": "0.9.2",
"source": "AGENTS.md section 3.3; SPEC.md section 5.1 and the v0.10.0 ledger; dev session 2026-08-31"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "queue half: csift search '' @<session> --no-subagents -t user.queued | rg -c 'popAll' (texts recalled into the input box); esc-recall-without-resend half: a live trial (send, esc, never resend) followed by a diff of the transcript before/after - structurally no record can appear",
"observed": "8 popAll recalls in the live session's queue lines (texts that returned to the input box; a popAll'd text that was then edited never becomes a user record); the esc-recall-without-resend half was not trialed in this audit and cannot be observed from the corpus by construction.",
"rule": "popAll count = queue-operation lines with operation popAll in one session; the negative half is a design statement, decidable only by a live trial.",
"note": "One half observed (8 specimens), the other half is unobservable from disk by construction and is stated as such."
}
]
},
{
"id": "WIN-001",
"area": "windows",
"behavior": "A Windows session's projects-directory basename is letter-led rather than dash-led: the sanitizer applies no slash, case or colon pre-normalization, so the cwd `C:\\Users\\x` encodes to `C--Users-x` (the drive letter passes through; `:` and `\\` each become exactly one dash) and a UNC root `\\\\server\\share` encodes to `--server-share`.",
"depends": "csift's `@`-token grammar has to recognize both shapes: `is_drive_encoded_token` accepts `<letter>--...` as an encoded project dir (bare or `@`-prefixed) while the UNC form is reachable only as `@--server-...`, because a bare `--`-leading token is reserved for the mistyped-flag guard; a drive-shaped token that matches no directory falls through to real-path resolution, since it can also be a genuine relative path. Without those arms a Windows project directory is unreachable by any target form.",
"code": [
{
"path": "src/path/home.rs",
"lines": "9-13",
"snippet": "/// `replace(/[^a-zA-Z0-9]/g,\"-\")` runs per UTF-16 CODE UNIT: every unit outside ASCII\n/// alphanumerics becomes ONE `-` - so an astral char (two surrogate units) yields TWO\n/// dashes, and a Windows `C:\\Users\\x` yields `C--Users-x` (`:` and `\\` are one unit\n/// each). No dash collapsing, no case folding. A char-wise or byte-wise replacement\n/// DIVERGES from CC on any non-ASCII cwd and resolves the wrong dir."
},
{
"path": "src/path/home.rs",
"lines": "147-154",
"snippet": "/// The Windows drive-letter encoded shape: `<letter>--…` (`C:\\Users\\x` → `C--Users-x`).\n/// Distinct from every other token class: a Unix encoded dir leads with `-`, an id is\n/// hex/uuid-shaped, and a real RELATIVE path named like this is disambiguated by the\n/// caller (encoded-dir lookup first, real-path fallthrough on a miss).\npub(crate) fn is_drive_encoded_token(token: &str) -> bool {\n let b = token.as_bytes();\n b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b'-' && b[2] == b'-'\n}"
},
{
"path": "src/path/resolver.rs",
"lines": "182-186",
"snippet": " // `@-Users-…` / `@C--Users-…` → an encoded project-dir name (a Unix cwd's\n // leading `/` encodes to `-`; a Windows cwd's `C:\\` encodes to `C--`).\n _ if id.starts_with('-') || is_drive_encoded_token(id) => {\n explicit_dirs.push(resolve_target(Path::new(id))?);\n session_target = true;"
}
],
"instrument": "Only a Claude Code session started on Windows produces the directory: run one and list `~/.claude/projects` - the basename must lead with the drive letter. Platform-independently the encoder is pinned by the csift unit tests in `src/path/tests/encode.rs` (`C--Users-dev-proj` from the drive form, plus the `--server-share-proj` UNC token) and the target grammar by `csift list @C--Users-dev-proj --claude-home <fixture>`. Counting rule: one output dash per non-alphanumeric UTF-16 code unit, so `C:\\` yields `C--`.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.3",
"source": "SPEC.md section 2.1; AGENTS.md section 3.1"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{140}replace\\(/\\[\\^a-zA-Z0-9\\]/g,\"-\"\\).{0,200}' + node -e on the extracted regex + csift list @C--Users-dev-proj --claude-home <fixture>",
"observed": "binary carries the sanitizer verbatim: 'var IL=200; function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")} function KA(e){let n=k(e); if(n.length<=IL) return n; return `${n.slice(0,IL)}-${be(e)}`}', exported as 'IL as MAX_SANITIZED_LENGTH'; running that exact regex gives C:\\Users\\x -> C--Users-x (10 chars), \\\\server\\share -> --server-share (14), /a/b/c_d -> -a-b-c-d, /a/.claude/x -> -a--claude-x; csift on a fixture home resolved 'C--Users-dev-proj' bare AND as '@C--Users-dev-proj' (both printed the session with cwd C:\\Users\\dev\\proj), resolved '@--server-share-proj', rejected a bare '--server-share-proj' with \"not a project target; did you mistype a flag? (a UNC-encoded dir is targeted as '@--server-share-proj')\", and fell through to real-path resolution for 'Z--nope-nope'",
"rule": "one output dash per non-alphanumeric UTF-16 code unit, no collapsing, no case folding; one target form per csift invocation, 5 forms exercised",
"note": "The encoder is decided without a Windows session because it is a pure function of the extracted regex: no slash, colon or case pre-normalization, and the drive letter survives because it is alphanumeric. The 200-char cap plus a hash suffix is present in the same function. All three csift code sites match verbatim at the claimed lines (src/path/home.rs 9-13 and 147-154, src/path/resolver.rs 182-186)."
}
]
},
{
"id": "WIN-002",
"area": "windows",
"behavior": "Claude Code places its `.claude` config home under the home directory resolved with Node `os.homedir()` semantics: `$HOME` on Unix, but ONLY `%USERPROFILE%` on Windows - the Windows branch never consults `HOME`, even though Git-Bash and MSYS shells export one, often a POSIX-style spelling a native process cannot use.",
"depends": "csift's `home_dir()` is cfg-split the same way (Unix reads `HOME`, Windows reads `USERPROFILE`, with the standard library's home lookup as the fallback), so a stray Git-Bash `HOME` never sends csift to a `.claude` directory Claude Code does not write; honoring it would make every subcommand read an empty or nonexistent projects root.",
"code": [
{
"path": "src/path/home.rs",
"lines": "29-35",
"snippet": "/// The user's home directory, resolved the way Claude Code itself resolves it (Node's\n/// `os.homedir()`): `$HOME` on Unix, `%USERPROFILE%` on Windows. The per-platform split is\n/// load-bearing on Windows - CC never consults `HOME` there, but Git-Bash/MSYS shells\n/// export one (often a POSIX-style `/c/Users/...` a native process cannot use), and\n/// honoring it would point csift at a `.claude` dir CC never writes. The conventional env\n/// var is read first so a test harness can relocate home per-subprocess; `std::env::home_dir`\n/// (un-deprecated, Windows-correct since Rust 1.85 - MSRV is above both) is the fallback."
},
{
"path": "src/path/home.rs",
"lines": "36-48",
"snippet": "pub(crate) fn home_dir() -> Result<PathBuf> {\n #[cfg(not(windows))]\n if let Some(h) = std::env::var_os(\"HOME\") {\n if !h.is_empty() {\n return Ok(PathBuf::from(h));\n }\n }\n #[cfg(windows)]\n if let Some(h) = std::env::var_os(\"USERPROFILE\") {\n if !h.is_empty() {\n return Ok(PathBuf::from(h));\n }\n }"
}
],
"instrument": "In a Git-Bash shell on Windows with `HOME` set to a POSIX-style path, print both `HOME` and `USERPROFILE` and compare them against where `~/.claude/projects` actually exists; `csift list` must still read the profile-directory `.claude`. Only a Windows Git-Bash shell exposes the divergence. Counting rule: one resolution per shell.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.1",
"source": "SPEC.md section 2.1; AGENTS.md section 7"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "unverifiable-here",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function s\\(\\)\\{return process\\.env\\.CLAUDE_CONFIG_DIR\\}var Se=.{0,80}' + HOME=<tmp> node -e 'console.log(require(\"os\").homedir())' + env -u CLAUDE_CONFIG_DIR HOME=<fixture> csift list",
"observed": "binary: 'function s(){return process.env.CLAUDE_CONFIG_DIR}var Se=Zo(()=>(s()??i(R(),\".claude\")).normalize(\"NFC\"),s)' with 'import{homedir as R}' - so the config home is CLAUDE_CONFIG_DIR, else join(os.homedir(), \".claude\"), NFC-normalized; on this unix host os.homedir() returned the overridden HOME, and csift with HOME pointed at a fixture listed that fixture's session",
"rule": "one home resolution per process; the divergence needs a shell where HOME and USERPROFILE differ, which only exists on Windows",
"note": "The unix half is confirmed (Claude Code joins '.claude' onto os.homedir(); os.homedir() honors HOME on unix; csift agrees). The load-bearing half - that the Windows branch consults only %USERPROFILE% and never HOME - cannot be decided on this host: this binary is a macOS build and os.homedir() is native code. What would decide it: a Windows Git-Bash shell with HOME set to a POSIX-style spelling different from %USERPROFILE%; print both, then check which one ~/.claude/projects actually lives under and whether csift list reads it. Both csift code sites match verbatim (src/path/home.rs 29-35 and 36-48)."
}
]
},
{
"id": "WIN-003",
"area": "windows",
"behavior": "On Windows Claude Code ships a SEPARATE first-class tool literally named `PowerShell` beside `Bash`, `BashOutput` and `KillShell` in its tool registry; it carries the invocation in the same `input.command` field and is described as executing a given PowerShell command with an optional timeout.",
"depends": "csift must treat it as a shell tool wherever a shell tool matters: `@trap` self-identification matches both names (a Bash-only gate left `@trap` blind exactly in the bashless Windows fallback), the background-task scan ingests `run_in_background` launches from both, and `--count-by tool` reports `PowerShell` as its own census key.",
"code": [
{
"path": "src/path/trap.rs",
"lines": "324-331",
"snippet": " // Both SHELL tools carry the invocation in `input.command`: `Bash`\n // everywhere, and Windows' SEPARATE `PowerShell` tool (CC 2.1.228:\n // the fallback when Git-for-Windows bash is absent, or the gated\n // preference - same `command` field, verbatim from the binary's\n // tool registry). A Bash-only gate left @trap blind exactly on the\n // mandatory Windows fallback.\n if (n == \"Bash\" || n == \"PowerShell\")\n && inp"
},
{
"path": "src/live/background_scan.rs",
"lines": "52-57",
"snippet": " input: Some(input),\n ..\n } if (matches!(name.as_str(), \"Bash\" | \"PowerShell\")\n && input\n .get(\"run_in_background\")\n .and_then(serde_json::Value::as_bool)"
}
],
"instrument": "Run `strings` over the installed Claude Code binary for the version under test and look for the tool-registry array holding `Bash`, `BashOutput`, `KillShell` and `PowerShell`, and for the tool description text `Executes a given PowerShell command`. On a Windows session, `csift search '' @<session> --count-by tool` lists `PowerShell` as its own key. Counting rule: one census key per tool `name`, counted over tool_use blocks.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.4",
"source": "SPEC.md section 6 v0.7.4 ledger; AGENTS.md section 3.9; CHANGELOG 0.7.4"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Executes a given PowerShell command' and rg '\"BashOutput\",\"KillShell\"' + csift search '' @<fixture-session> --claude-home <fixture> --count-by tool + csift whoami @trap:<marker> --claude-home <fixture>",
"observed": "tool registry verbatim: 'var ymo=[\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",...]'; description verbatim: 'Executes a given PowerShell command with optional timeout. Working directory persists between commands; shell state (variables, functions) does not.'; on a synthetic Windows-shaped transcript csift's census printed '2 Bash / 2 PowerShell / 2 Write' - PowerShell is its own key - and @trap resolved through a PowerShell tool_use whose input.command carried the marker",
"rule": "one registry entry per tool name; one census key per tool name counted over tool_use blocks; one trap resolution per marker",
"note": "PowerShell sits between KillShell and Tmux in the registry array, i.e. it is a first-class tool and not an alias. The same input.command field is asserted by the description text and by csift's trap resolving on it. Both csift code sites match verbatim (src/path/trap.rs 324-331, src/live/background_scan.rs 52-57)."
}
]
},
{
"id": "WIN-004",
"area": "windows",
"behavior": "The Windows shell tool is selected in a fixed order: CLAUDE_CODE_USE_POWERSHELL_TOOL as an explicit override (any defined value wins, not just a truthy one), else PowerShell is FORCED ON when the Git-for-Windows bash lookup returns null, else a named feature gate decides. The bash lookup itself has FOUR steps, not three: CLAUDE_CODE_GIT_BASH_PATH (accepted only when its basename is bash.exe/sh.exe/bash/sh AND the file exists, otherwise a warning is logged and auto-detection continues), then C:\\Program Files\\Git\\bin\\bash.exe, then the (x86) sibling, then a bash.exe derived from a git executable found on PATH (join(gitdir,'..','..','bin','bash.exe')).",
"depends": "csift cannot assume a Windows transcript uses `Bash`: the bashless fallback is mandatory rather than optional, so every shell-tool consumer must accept both names or go blind on exactly the machines that have no bash installed.",
"code": [
{
"path": "src/path/trap.rs",
"lines": "322-331",
"snippet": " } = b\n {\n // Both SHELL tools carry the invocation in `input.command`: `Bash`\n // everywhere, and Windows' SEPARATE `PowerShell` tool (CC 2.1.228:\n // the fallback when Git-for-Windows bash is absent, or the gated\n // preference - same `command` field, verbatim from the binary's\n // tool registry). A Bash-only gate left @trap blind exactly on the\n // mandatory Windows fallback.\n if (n == \"Bash\" || n == \"PowerShell\")\n && inp"
}
],
"instrument": "strings -n 6 <claude-versions-dir>/<version> and read the selection function and the bash-discovery function whole, rather than only checking that both env names appear; the fourth probe and the env-override validation are only visible in the full function body.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.4",
"source": "SPEC.md section 6 v0.7.4 ledger; AGENTS.md section 3.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Zk\\(\\)\\{.{0,200}' and rg -o 'function de\\(\\)\\{let\\{existsSync:e\\}=ce\\(\\).{0,900}'",
"observed": "selection verbatim: 'function Zk(){let e=a.CLAUDE_CODE_USE_POWERSHELL_TOOL; if(D()!==\"windows\")return e===!0; if(e!==void 0)return e; if(B1()===null)return!0; return P(\"tengu_cobalt_ridge\",!1)}' and 'function cs(){if(D()!==\"windows\")return!0; return B1()!==null}' / 'function HD(){return cs()?\"bash\":\"powershell\"}'; bash discovery verbatim: env override validated by 'L.basename(...).toLowerCase()' being in ['bash.exe','sh.exe','bash','sh'] AND existsSync, else a warn 'CLAUDE_CODE_GIT_BASH_PATH \"...\" not found / is not a bash/sh binary; falling back to auto-detection'; then r=['C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe','C:\\\\Program Files (x86)\\\\Git\\\\bin\\\\bash.exe']; then a which-style lookup of 'git' joined as join(n,'..','..','bin','bash.exe'); returns null if none exist",
"rule": "one probe per candidate path, tried in source order; presence of both env names on the selection path",
"note": "The three-step selection order in the claim is exactly right. Two refinements: (1) the env override is honored whenever it is DEFINED (the code returns its value, so an explicit falsey value forces the tool OFF, it does not fall through); (2) the git-bash probe has a fourth step - deriving bash.exe from a git on PATH - and the env path is validated by basename and existence with a warn-and-continue, so a bad CLAUDE_CODE_GIT_BASH_PATH does not by itself force PowerShell on. The forced-on branch still cannot be executed here; it needs a Windows host with no Git-for-Windows bash. The csift code site matches verbatim (src/path/trap.rs 322-331)."
}
]
},
{
"id": "WIN-005",
"area": "windows",
"behavior": "The Windows `Bash` tool runs the real Git-for-Windows (MSYS2) bash, so the command text on those records is POSIX syntax; the `PowerShell` tool's command text is not, and no lexical bash analysis is valid on it.",
"depends": "csift's bash-lexical layers stay valid on Windows `Bash` records but deliberately do NOT run on `PowerShell` records: the dangerous-rm escalation classifier and the bash mutation attribution both skip them, so a pending PowerShell lane classifies `awaiting-execution` rather than escalation-blocked, and PowerShell shell-side file mutations surface as opaque rows instead of attributed ones. Structured Read/Write/Edit attribution is unaffected.",
"code": [
{
"path": "src/bash_danger.rs",
"lines": "84-91",
"snippet": "/// STALENESS NOTE (binary evidence, 2026-08-12): CC 2.1.228's classifier (`aLa`) has\n/// EVOLVED past the 2.1.x generation this port mirrors - it strips `$(…)` groups to a\n/// FIXPOINT (this port is single-pass), and a tree-sitter pass bails to explicit approval\n/// when a command carries >64 command substitutions. csift's escalation-blocked prediction\n/// can therefore diverge from current CC on those shapes; a port refresh is a recorded\n/// follow-up, not silent drift. (CC also ships a separate Windows `PowerShell` tool; this\n/// lexical-bash classifier deliberately does NOT run on PowerShell commands - a pending\n/// PowerShell lane classifies awaiting-execution.)"
},
{
"path": "src/recover/scan.rs",
"lines": "366-374",
"snippet": " } else if name == \"PowerShell\" && cmd.is_some() {\n out.push(OpaqueCommand {\n session_id: session_id.to_string(),\n line_no,\n turn_index,\n timestamp_utc: rec.timestamp.clone(),\n marker: \"powershell\".to_string(),\n });\n }"
}
],
"instrument": "On a Windows session, `csift files @<session> --by timeline` shows structured Read/Write/Edit rows and zero bash-heuristic rows for PowerShell commands, while `csift recover` reports those commands in its opaque per-window accounting under the `powershell` marker. Counting rule: one opaque row per PowerShell tool_use carrying a command.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.4",
"source": "AGENTS.md section 3.9; SPEC.md section 6 v0.7.4 ledger"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "holds",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{40}Using bash path.{0,200}' + csift agents @<fixture-session> --claude-home <fixture> --format json + csift files @<fixture-session> --by timeline + csift recover @<fixture-session> --file 'C:\\Users\\dev\\proj\\src\\app.ts' --coverage",
"observed": "binary verbatim: 'function tkt(){if(D()===\"windows\"){let e=B1(); if(e)process.env.SHELL=e,t(`Using bash path: \"${e}\"`); else t(\"Git Bash not found; BashTool will be unavailable\")}}' - the Windows Bash tool's shell IS the discovered Git-for-Windows bash, and without it the tool is withdrawn. On two sibling subagent lanes each ending in an unreturned shell tool_use, csift classified {'pending_tool_name':'PowerShell','pending_classification':'awaiting-execution'} and {'pending_tool_name':'Bash','pending_classification':'escalation-blocked'} for the equivalent recursive-force delete. files --by timeline listed exactly 1 mutation (the structured Write) and zero rows for the PowerShell Set-Content. recover --coverage printed 'opaque in window: 1 PowerShell command(s), never parsed' with the row 'L2 turn 0 ... powershell'.",
"rule": "one pending classification per frozen lane; one mutation row per attributed operand; one opaque row per PowerShell tool_use carrying a command",
"note": "The lexical-bash split is instrumented directly rather than argued: the SAME shape of destructive command classifies escalation-blocked under Bash and awaiting-execution under PowerShell, and a PowerShell write idiom that the bash heuristics would have attributed instead surfaces as an opaque 'powershell' row. Both csift code sites match verbatim (src/bash_danger.rs 84-91, src/recover/scan.rs 366-374)."
}
]
},
{
"id": "WIN-006",
"area": "windows",
"behavior": "Claude Code writes Windows paths into structured tool fields in native form (`C:\\...`), so on a Windows transcript the absolute path shapes are a leading `/`, a `<letter>:` drive prefix, or a `\\\\server\\...` UNC prefix - and a Windows `Bash` record's POSIX-style command text is therefore not joinable with the native `filePath` values of the same session.",
"depends": "csift's `is_absolute_shell_path` accepts all three shapes before deciding whether to join an operand to the carrying record's `cwd` (treating `C:\\...` as relative would fabricate a path in every `files`/`recover` row), and `join_shell_path` joins in the BASE's separator family - a Windows-family base with `\\`, a unix base with `/` - so host path semantics never leak into the analysis and a transcript from either platform is readable on either platform.",
"code": [
{
"path": "src/bash_mutations/cwd.rs",
"lines": "206-215",
"snippet": "/// True for a path the shell treats as absolute: unix-rooted, a Windows drive form\n/// (`C:\\` or `C:/`), or a UNC `\\\\server\\...` prefix.\n#[must_use]\npub fn is_absolute_shell_path(p: &str) -> bool {\n if p.starts_with('/') || p.starts_with(\"\\\\\\\\\") {\n return true;\n }\n let b = p.as_bytes();\n b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && matches!(b[2], b'/' | b'\\\\')\n}"
},
{
"path": "src/bash_mutations/cwd.rs",
"lines": "217-221",
"snippet": "/// Join path fragments onto an absolute base and normalize `.`/`..` lexically, as pure\n/// string work in the BASE's separator family. Transcripts are analyzed on any host, so\n/// host `Path` semantics must never leak in: a Windows-family base joins with `\\` and\n/// converts `/` in the fragments; a unix base joins with `/`. `..` never pops past the\n/// root (the lexical-normalize convention used elsewhere in the crate)."
}
],
"instrument": "The unit test `is_absolute_shell_path_covers_all_three_families` in `src/bash_mutations/tests/cwd.rs` pins the drive, UNC and unix families (and rejects a driveless `C:relative`); on a real Windows session, `csift files @<session> --format json` rows must carry drive-led paths with an explicit resolution class and no cwd-joined fabrication. Counting rule: one resolution class per mutation operand.",
"located": {
"claude_code": "2.1.228",
"csift": "0.7.4",
"source": "AGENTS.md section 3.9; dev session measurement of structured path shapes"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "unverifiable-here",
"instrument": "csift files @<fixture-session> --claude-home <fixture> --by timeline --format json, run twice against two fixtures identical except for the record cwd (C:\\Users\\dev\\proj vs /home/dev/proj), each with the same relative bash redirect (a printf into src/out.txt) + ls ~/.claude/projects | grep -cE '^[A-Za-z]--'",
"observed": "windows-family base produced {'path':'C:\\\\Users\\\\dev\\\\proj\\\\src\\\\out.txt','op':'bash','resolution':'cwd-joined'} and the unix base {'path':'/home/dev/proj/src/out.txt','op':'bash','resolution':'cwd-joined'} - same macOS host, separator family taken from the base, not the host; a structured Write of C:\\Users\\dev\\proj\\src\\app.ts passed through verbatim with resolution null (no cwd join, no fabrication). Corpus: 0 of 15 project directories are drive-letter-led, so no real Windows transcript exists here.",
"rule": "one resolution class per mutation operand; one directory per listing entry",
"note": "The csift half is instrumented and behaves as claimed. The Claude Code half - that a Windows session writes native C:\\... into the record cwd and into structured tool fields such as toolUseResult.filePath, and that the Windows Bash tool's POSIX command text is therefore not joinable with them - cannot be decided on this host: there is no Windows transcript in the corpus and the fixtures above are hand-written, so they prove csift's handling, not Claude Code's wire shape. What would decide it: one transcript under a drive-letter-led project directory; read its top-level cwd field and one Bash record's input.command side by side. Both csift code sites match verbatim (src/bash_mutations/cwd.rs 206-215 and 217-221), and the named unit test is present in src/bash_mutations/tests/cwd.rs."
}
]
},
{
"id": "WIN-007",
"area": "windows",
"behavior": "A session registry row carries the owner process's creation instant under one of TWO optional keys: `procStart` (an asctime-like UTC string, `Sun Aug 16 09:04:23 2026`) or `procStartFt` (a FILETIME integer - 100-nanosecond ticks since 1601-01-01, e.g. `134328101803820142`). The row schema declares both optional, a platform-conditional splitter writes the value to exactly one of them, and every reader in the binary resolves `procStartFt ?? procStart`. The rendering is not pinned to the key: unix rows carry the asctime form under `procStart`, and a Windows 11 ARM64 session measured at 2.1.258 wrote its FILETIME under `procStart` as well.",
"depends": "csift reads `procStartFt` first and falls back to `procStart`, then parses EITHER rendering into an instant before comparing it with the probed process start (an all-digits value is the FILETIME form, otherwise the asctime parse), so the pid-reuse guard works whichever key and whichever rendering a row carries; reading a single key, or parsing a single format, would silently degrade every Windows row to a disclosed pid-only probe.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "13-21",
"snippet": "//! `procStart` is the OWNER PROCESS's creation instant in a PLATFORM-SPECIFIC rendering:\n//! on unix an asctime string in UTC (`Sun Aug 16 09:04:23 2026`), on Windows a FILETIME\n//! integer (100ns ticks since 1601-01-01, e.g. `134328101803820142`). `pidDomain` names\n//! the pid space the row was written in (`darwin`, `linux`, or `win32:<hostname>`); a row\n//! from another domain cannot be probed here and the verdict says so. `ps lstart` renders\n//! in the LOCAL zone - a naive string/local comparison flags pid reuse on EVERY row.\n//! Parse both sides to instants and compare with a small tolerance; when either side is\n//! absent or unparseable, degrade to a pid-only probe AND say so in the evidence (the\n//! reuse guard was skipped, honest, never silent)."
},
{
"path": "src/live/registry.rs",
"lines": "175-188",
"snippet": "/// Parse the registry's `procStart`: an asctime-like UTC string (`Sun Aug 16 09:04:23\n/// 2026`, unix) or a FILETIME integer (`134328101803820142`, Windows: 100ns ticks since\n/// 1601). `None` on any mismatch - the caller degrades to pid-only + a note.\npub(crate) fn parse_registry_proc_start(s: &str) -> Option<jiff::Timestamp> {\n let s = s.trim();\n if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {\n return filetime_to_timestamp(s.parse::<u64>().ok()?);\n }\n let bd = jiff::fmt::strtime::parse(\"%a %b %e %H:%M:%S %Y\", s).ok()?;\n let dt = bd.to_datetime().ok()?;\n dt.to_zoned(jiff::tz::TimeZone::UTC)\n .ok()\n .map(|z| z.timestamp())\n}"
},
{
"path": "src/live/registry.rs",
"lines": "190-199",
"snippet": "/// A Windows FILETIME (100ns ticks since 1601-01-01 UTC) as an instant; `None` when the\n/// value cannot be a real creation time (before the unix epoch or absurdly far out).\npub(crate) fn filetime_to_timestamp(ticks: u64) -> Option<jiff::Timestamp> {\n let secs = i64::try_from(ticks / 10_000_000).ok()? - FILETIME_UNIX_OFFSET_SECS;\n if secs < 0 {\n return None;\n }\n let nanos = i32::try_from((ticks % 10_000_000) * 100).ok()?;\n jiff::Timestamp::new(secs, nanos).ok()\n}"
},
{
"path": "src/live/registry.rs",
"lines": "72-77",
"snippet": " // The 2.1.258 schema carries TWO keys and every reader in the binary is\n // `procStartFt ?? procStart` (a platform splitter writes exactly one of\n // them; the Windows session measured live wrote the FILETIME under\n // `procStart`, so both spellings are read and both renderings parse).\n proc_start: str_field(\"procStartFt\").or_else(|| str_field(\"procStart\")),\n pid_domain: str_field(\"pidDomain\"),"
}
],
"instrument": "Read `procStart` from every row under `~/.claude/sessions/*.json` and classify it: an all-ASCII-digit value is the Windows FILETIME rendering, anything else is the unix asctime form. Measured 2026-09-02 on this machine: 9 rows, 9 asctime, 0 FILETIME (a unix host). Counting rule: one classification per row file; a FILETIME row requires a registry written by a Windows session.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.1",
"source": "src/live/registry.rs module doc; SKILL.md status/wait section"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "drifted",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function OU\\(e\\)\\{.{0,120}' ; rg -o 'async function m\\(e,t\\)\\{let n=t===void 0.{0,900}' ; rg -c 'ToFileTimeUtc|CreationDate|Get-CimInstance' + python3 json read of procStart across ~/.claude/sessions/*.json",
"observed": "the persisted schema carries TWO keys: '{peerToken:..., procStart: optional string, procStartFt: optional string, pidDomain: optional string}', split by 'function OU(e){return c()?{procStart:void 0,procStartFt:e}:{procStart:e,procStartFt:void 0}}' and read back everywhere as 'r.procStartFt ?? r.procStart'; the <pid>.json row reader accepts both ('procStart: typeof o.procStart===\"string\"?o.procStart:void 0, ...typeof o.procStartFt===\"string\"&&{procStartFt:o.procStartFt}'). The ONLY proc-start producer in the binary is ps: two call sites, both 'ps -o lstart= -p <pid>' with env LC_ALL=C, TZ=UTC. ToFileTimeUtc 0 hits, CreationDate 0 hits, Get-CimInstance 0 hits; the 4 Get-Process and 6 tasklist hits are a PowerShell alias table, a permission-prompt placeholder, an IDE-detection command and a command safe-flag allowlist - none is a process-start probe. On disk: 8 rows, 8 asctime procStart values, 0 all-digit values, 0 rows carrying procStartFt.",
"rule": "one classification per row file (all-ASCII-digit = FILETIME rendering, else asctime); one producer per distinct proc-start call site in the binary",
"note": "Current Claude Code does not put a FILETIME under 'procStart'. It carries a SECOND key, 'procStartFt', beside it, and a platform-conditional splitter writes the value to exactly one of them - on the FILETIME platform the value goes to procStartFt and procStart is left undefined, and the paired reader explicitly REFUSES a procStart value there ('if(c())return e.procStart!==void 0?void 0:e.procStartFt'). Every reader in the binary is 'procStartFt ?? procStart'. csift reads only 'procStart' (src/live/registry.rs registry_row_for), so its all-digit FILETIME branch in parse_registry_proc_start cannot fire on a current Windows row and the reuse guard would silently degrade to pid-only there. Separately, no FILETIME producer exists anywhere in this build - the only acquisition is ps -o lstart= - so the FILETIME rendering may now be reachable only on code paths this macOS build does not contain; that half stays undecided here. All three csift code sites still match verbatim (src/live/registry.rs 13-21, 171-184, 186-195), so the drift is in the module doc's field name and platform story, not in a moved snippet. What would settle the remainder: one ~/.claude/sessions/<pid>.json written by a Windows session - read whether it carries procStart, procStartFt, or neither."
}
]
},
{
"id": "WIN-008",
"area": "windows",
"behavior": "A session registry row names the pid space it was written in via pidDomain, whose head is the platform token and whose tail disambiguates the pid space: bare 'darwin' on macOS (and on any host that is neither linux, wsl nor windows), 'win32:<hostname lowercased>' on Windows, and 'linux:<contents of /etc/machine-id>:<readlink of /proc/self/ns/pid>' on linux and wsl. The field is absent on older rows.",
"depends": "csift compares the row's domain head against the local one and returns a foreign-domain verdict WITHOUT probing when they differ - a pid from another machine or operating system means nothing locally, so `stale-dead` is undecidable and the evidence row says exactly that instead of guessing.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "109-122",
"snippet": "/// The pid domain this csift runs in, in the registry's own vocabulary (the prefix\n/// before any `:<hostname>` suffix).\n#[must_use]\npub(crate) fn local_pid_domain() -> &'static str {\n if cfg!(target_os = \"macos\") {\n \"darwin\"\n } else if cfg!(target_os = \"linux\") {\n \"linux\"\n } else if cfg!(windows) {\n \"win32\"\n } else {\n \"unknown\"\n }\n}"
},
{
"path": "src/live/registry.rs",
"lines": "134-139",
"snippet": " if let Some(d) = pid_domain {\n let head = d.split(':').next().unwrap_or(d);\n if head != local_pid_domain() {\n return PidLiveness::ForeignDomain(d.to_string());\n }\n }"
},
{
"path": "src/live/verdict.rs",
"lines": "205-212",
"snippet": " PidLiveness::ForeignDomain(d) => (\n format!(\"row from another pid domain ({d})\"),\n Some(\n \"the registry row was written in another pid domain (another machine \\\n or OS): its pid means nothing here - stale-dead is undecidable\"\n .to_string(),\n ),\n ),"
}
],
"instrument": "Read pidDomain from every row under ~/.claude/sessions/*.json (measured 2026-09-02: 8 rows, 5 'darwin', 3 absent), and read the producer out of the binary for the platform branches this host cannot write. Counting rule: one value per row file; a win32 or linux value requires a row written on that platform.",
"located": {
"claude_code": "2.1.258",
"csift": "0.10.1",
"source": "src/live/registry.rs module doc; SKILL.md status/wait section"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function YG\\(\\)\\{return wie\\.pidDomain.{0,420}' ; rg -o '[^.a-zA-Z]process\\.platform' | wc -l ; rg -o 'process\\.env\\.' | wc -l + python3 json read of pidDomain across ~/.claude/sessions/*.json",
"observed": "producer verbatim: 'function YG(){return wie.pidDomain??=(async()=>{let n=D(); if(n===\"windows\")return `darwin:${e().toLowerCase()}`; if(n!==\"linux\"&&n!==\"wsl\")return\"darwin\"; let[t,r]=await Promise.all([a(\"/etc/machine-id\",\"utf8\").then((i)=>i.trim(),()=>\"\"),u(\"/proc/self/ns/pid\").catch(()=>\"\")]); return `darwin:${t}:${r}`})()...}' with 'import{hostname as e}from\"os\"'. The literal 'darwin' is the build-inlined platform constant: this build has 0 bare process.platform and 0 bare process.arch occurrences in code against 836 process.env. occurrences, while two globalThis.process.platform member expressions survive - the signature of a targeted build-time define on the bare identifier only. On disk: 8 rows, 5 'darwin', 3 with no pidDomain field.",
"rule": "one value per row file; one branch per platform in the producer; the define is established by counting bare vs globalThis-qualified process.platform occurrences",
"note": "The claim's Windows form is right, but its vocabulary list is wrong for linux: a linux or wsl row is NOT bare 'linux', it is a three-segment 'linux:<machine-id>:<pid-namespace>'. This does not break csift, which compares only the head before ':' (local_pid_domain plus the split in src/live/registry.rs 130-135), but the module doc and the SKILL wording should say so. The row counts also moved since the ledger was written (9 rows / 6 darwin then, 8 rows / 5 darwin now - registry rows are pid-keyed and get swept). The win32 head itself is inferred from the build-define rather than read off a Windows row; what would settle it directly is one ~/.claude/sessions/<pid>.json written by a Windows session. All three csift code sites match verbatim (src/live/registry.rs 105-118 and 130-135, src/live/verdict.rs 205-212)."
}
]
},
{
"id": "WIN-009",
"area": "windows",
"behavior": "Process liveness has no portable probe. On unix `ps -p PID -o lstart=` answers both existence and start time, except that busybox ps rejects `-p`/`lstart` outright and so fails even for a LIVE pid, where Linux `/proc/<pid>` answers liveness directly. Windows has no such `ps` at all: existence plus the creation FILETIME come from a PowerShell `Get-Process -Id` query, and `tasklist` answers existence alone.",
"depends": "csift's probe is a platform chain that never fabricates a verdict: unix tries `ps` then `/proc`; Windows tries PowerShell `Get-Process` (start time via `ToFileTimeUtc`, a `NOSTART` sentinel when another user's process hides it) then `tasklist`; and when no probe tool exists - including on a host that is neither unix nor Windows - the verdict is `Unavailable`, stating that pid liveness cannot be checked here rather than reporting a live session `stale-dead`.",
"code": [
{
"path": "src/live/registry.rs",
"lines": "97-99",
"snippet": " /// No process probe exists on this host (neither `ps`, `/proc`, PowerShell nor\n /// `tasklist` answered): the verdict must say so.\n Unavailable,"
},
{
"path": "src/live/registry.rs",
"lines": "213-222",
"snippet": " if !out.status.success() || text.is_empty() {\n // busybox ps (Alpine and friends) rejects `-p`/`lstart` outright, so the probe\n // fails for a LIVE pid too. On Linux `/proc/<pid>` answers liveness directly:\n // present = alive with the start time unknown (the reuse-guard skip is\n // disclosed); absent (or no /proc at all, as on macOS where the ps form is\n // reliable) = the no-such-process verdict stands.\n if std::path::Path::new(&format!(\"/proc/{pid}\")).is_dir() {\n return PsProbe::Alive(None);\n }\n return PsProbe::NoProcess;"
},
{
"path": "src/live/registry.rs",
"lines": "237-244",
"snippet": "/// Windows: one PowerShell call answers both questions - `Get-Process -Id` fails (exit\n/// 3 by our script) for a missing pid, and `StartTime.ToFileTimeUtc()` yields the\n/// creation FILETIME (the registry's own rendering) when the process is ours to inspect;\n/// `NOSTART` when the start time is not readable (another user's process) = alive, guard\n/// skipped. When PowerShell cannot be spawned, `tasklist` answers liveness alone; when\n/// neither tool exists the probe is unavailable and the verdict says so.\n#[cfg(windows)]\npub(crate) fn ps_probe(pid: u32) -> PsProbe {"
},
{
"path": "src/live/registry.rs",
"lines": "264-270",
"snippet": " // PowerShell missing or broken: tasklist answers liveness (no start time).\n let Ok(out) = std::process::Command::new(\"tasklist\")\n .args([\"/FI\", &format!(\"PID eq {pid}\"), \"/NH\", \"/FO\", \"CSV\"])\n .output()\n else {\n return PsProbe::Unavailable;\n };"
},
{
"path": "src/live/registry.rs",
"lines": "279-282",
"snippet": "#[cfg(not(any(unix, windows)))]\npub(crate) fn ps_probe(_pid: u32) -> PsProbe {\n PsProbe::Unavailable\n}"
},
{
"path": "src/live/verdict.rs",
"lines": "213-216",
"snippet": " PidLiveness::Unavailable => (\n \"probe unavailable on this host\".to_string(),\n Some(\"pid liveness cannot be checked here - stale-dead is undecidable\".to_string()),\n ),"
}
],
"instrument": "On macOS ps -p $$ -o lstart= prints the LOCAL-zone instant in the '%a %e %b %H:%M:%S %Y' field order (day of month before month), which is csift's SECOND parse format - the registry's own procStart, written with TZ=UTC, uses the '%a %b %e ...' order instead. A probe test that pins only one order will pass on one platform and silently fall back to Alive(None) on the other. A missing pid is reported by a non-zero ps exit, not an empty stdout.",
"located": {
"claude_code": null,
"csift": "0.10.1",
"source": "src/live/registry.rs probe comments; SKILL.md status/wait section"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "ps -p $$ -o lstart= ; test -d /proc ; ps -p 999999 -o lstart= ; echo $? + csift status @main (reading the pid evidence row)",
"observed": "ps -p $$ -o lstart= printed 'Wed 2 Sep 19:59:08 2026' and exited 0; /proc is absent on this host; ps -p 999999 -o lstart= printed 'ps: process id too large: 999999' and exited 1, so the probe reaches csift's no-such-process arm via the non-zero exit rather than an empty success. csift status @main printed 'verdict running / registry status busy (pid <n>) / pid alive (start-time guard matched)' - the unix ps arm ran end to end and its local-zone reading matched the registry's UTC asctime procStart.",
"rule": "one probe outcome per platform; the pid evidence row names exactly one outcome and one guard state per status run",
"note": "The unix arm is instrumented end to end, including the cross-rendering comparison the module doc describes (local-zone ps output against a UTC-rendered registry value, guard reported as matched). Two arms were NOT executed on this host and remain code-only: the busybox fallback (needs a shell whose ps rejects -p/lstart, with /proc present) and the whole Windows chain (PowerShell Get-Process with the NOSTART sentinel, then tasklist, then Unavailable). All six csift code sites match verbatim (src/live/registry.rs 93-95, 209-218, 233-240, 260-266, 275-278 and src/live/verdict.rs 213-216)."
}
]
},
{
"id": "WIN-010",
"area": "windows",
"behavior": "No Windows Claude Code session exists in this development corpus: a scan finds ZERO project directories with a drive-letter-led encoded name and ZERO transcripts carrying a `PowerShell` tool_use, so every PowerShell-side wire shape csift handles is derived from the Claude Code binary rather than observed on disk.",
"depends": "csift's PowerShell handling is honest-skip by construction - a pending PowerShell lane classifies `awaiting-execution` and its command text is never lexically parsed, surfacing instead as an opaque `powershell` marker in recover's per-window accounting - but the background-launch grammar it assumes for PowerShell (the same `run_in_background` input flag and result text as Bash) is untested against real data.",
"code": [
{
"path": "src/recover/scan.rs",
"lines": "366-374",
"snippet": " } else if name == \"PowerShell\" && cmd.is_some() {\n out.push(OpaqueCommand {\n session_id: session_id.to_string(),\n line_no,\n turn_index,\n timestamp_utc: rec.timestamp.clone(),\n marker: \"powershell\".to_string(),\n });\n }"
}
],
"instrument": "ls ~/.claude/projects | grep -cE '^[A-Za-z]--' and csift search PowerShell -t agent.tool.use --count-by tool. Re-measured 2026-09-02 with csift 0.10.0: 0 drive-letter-led directories out of 15, and 0 PowerShell census keys among 219 matched records across 11 tool keys, over 7603 transcripts in scope (66 top-level, 7537 subagent). Counting rule: one directory per listing entry; one census key per tool name.",
"located": {
"claude_code": "2.1.258",
"csift": "0.7.4",
"source": "dev session 2026-09-02"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "ls ~/.claude/projects | grep -cE '^[A-Za-z]--' ; ls ~/.claude/projects | wc -l + csift search PowerShell -t agent.tool.use --count-by tool + csift list --format json (header row)",
"observed": "0 drive-letter-led directories out of 15 project directories; the census printed 91 Bash / 63 Edit / 23 Write / 22 StructuredOutput / 7 WebFetch / 4 WebSearch / 2 Agent / 2 SendMessage / 2 TaskCreate / 2 Workflow / 1 Monitor and the footer '219 matched record(s) across 11 tool key(s)' - no PowerShell key, so every hit is prose mentioning the word while riding some other tool; list reported sessions_in_scope 7603 (66 top-level, 7537 subagent)",
"rule": "one directory per listing entry; one census key per tool name, counted over tool_use blocks in records matching the pattern",
"note": "The finding stands - no Windows Claude Code session exists in this corpus, so every PowerShell-side wire shape csift handles is still derived from the binary rather than observed on disk. Only the numbers moved: 219 matched records rather than 190 (same 11 tool keys), because the corpus grew during the day the ledger was written. The corpus size is worth carrying with the claim so a later re-run can tell growth from a real Windows session appearing. The csift code site matches verbatim (src/recover/scan.rs 366-374)."
}
]
},
{
"id": "WIN-011",
"area": "windows",
"behavior": "Claude Code 2.1.258's dangerous-rm classifier strips `$(...)` and non-`$` `(...)` groups to a FIXPOINT and, on a separate tree-sitter pass, refuses to analyze a command carrying more than 64 substitution nodes: when the command text also matches `/\\brm(?:dir)?\\b/` it returns `behavior:\"ask\"` with `classifierApprovable:false` and `circuitBreaker:\"dangerousRemoval\"` (explicit approval, not bypassable), and otherwise returns no verdict. The counted node set is command_substitution plus process_substitution plus `${ ... }` funsub-shaped expansion/ERROR nodes, so the user-facing count labelled `command substitutions` is broader than `$( ... )` alone.",
"depends": "Only the >64 bail is a real port divergence: csift's `dangerous_rm` has no substitution-count gate, so on a command with more than 64 substitutions and an `rm`/`rmdir` word but no bare `$VAR/...` target, Claude Code hoists to explicit approval while csift predicts `awaiting-execution` rather than `escalation-blocked`. The fixpoint strip is NOT a divergence -- the port implements the same fixpoint loop. Both facts are disjoint from the PowerShell lane, on which this lexical-bash classifier never runs.",
"code": [
{
"path": "src/bash_danger.rs",
"lines": "84-91",
"snippet": "/// STALENESS NOTE (binary evidence, 2026-08-12): CC 2.1.228's classifier (`aLa`) has\n/// EVOLVED past the 2.1.x generation this port mirrors - it strips `$(…)` groups to a\n/// FIXPOINT (this port is single-pass), and a tree-sitter pass bails to explicit approval\n/// when a command carries >64 command substitutions. csift's escalation-blocked prediction\n/// can therefore diverge from current CC on those shapes; a port refresh is a recorded\n/// follow-up, not silent drift. (CC also ships a separate Windows `PowerShell` tool; this\n/// lexical-bash classifier deliberately does NOT run on PowerShell commands - a pending\n/// PowerShell lane classifies awaiting-execution.)"
},
{
"path": "src/bash_danger.rs",
"lines": "155-166",
"snippet": "fn strip_paren_groups(s: &str) -> String {\n let mut n = s.to_string();\n loop {\n let prev = n.clone();\n n = DOLLAR_PAREN.replace_all(&n, \" \").into_owned();\n n = remove_plain_groups(&n);\n if n == prev {\n break;\n }\n }\n n\n}"
}
],
"instrument": "Run `strings` over the installed Claude Code binary, locate the classifier's command-substitution handling, and compare it against `src/bash_danger.rs`: the fixpoint strip loop and the 64-substitution bail are present in the binary and absent from the port. Counting rule: one comparison per classifier generation.",
"located": {
"claude_code": "2.1.228",
"csift": null,
"source": "src/bash_danger.rs header comment; AGENTS.md section 3.9"
},
"first_seen_claude_code": null,
"checks": [
{
"claude_code": "2.1.258",
"csift": "0.10.1",
"date": "2026-09-02",
"verdict": "refined",
"instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > /tmp/cc258.strings.txt; rg -o 'function hnt\\(e\\)\\{if\\(!e\\.includes.{0,700}' /tmp/cc258.strings.txt; rg -o 'if\\(d\\(e\\),o\\.length>64\\)\\{if\\(/\\\\brm\\(\\?:dir\\)\\?\\\\b/\\.test\\(e\\.text\\)\\).{0,120}' /tmp/cc258.strings.txt; rg -o 'function sF\\(e,n,r\\)\\{return\\{behavior.{0,220}' /tmp/cc258.strings.txt; rg -o 'yto=/\\^.{0,120}' /tmp/cc258.strings.txt; strings -n 6 ~/.local/share/claude/versions/2.1.229 | rg -c 'too many to analyze for catastrophic removals'; rg -c '64' src/bash_danger.rs; sed -n '84,91p;155,166p' src/bash_danger.rs",
"observed": "2.1.258 binary, classifier `hnt`: fixpoint strip present verbatim -- `for(let o=\"\";o!==r;)o=r,r=r.replace(/\\$\\([^()]*\\)/g,\" \").replace(/(?<!\\$)\\([^()]*\\)/g,\" \");`. Substitution-count bail present verbatim -- `if(d(e),o.length>64){if(/\\brm(?:dir)?\\b/.test(e.text))return sF(\"rm\",`This command contains ${o.length} command substitutions - too many to analyze for catastrophic removals. This requires explicit approval.`,`- too many command substitutions to analyze (${o.length})`);return null}`. The bail's return builder is `function sF(e,n,r){return{behavior:\"ask\",message:n,decisionReason:{type:\"safetyCheck\",reason:`Dangerous ${e} operation ${r}`,classifierApprovable:!1,circuitBreaker:\"dangerousRemoval\"},suggestions:[]}}`. The two ported regexes are byte-identical in 2.1.258: `_to=/^(?:[A-Za-z_][A-Za-z0-9_]*\\+?=[^\\s]*\\s+)*\\\\?(?:[^\\s=]*\\/)?(rm|rmdir)(?:\\s|$)/` and `yto=/^\"?\\$(?:\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)\"?\\/(?:\\*|\\$|\\/|[\"']|$)/`. The 2.1.229 binary carries the same bail message (1 match), so both mechanisms predate 2.1.258. csift side: `rg -c '64' src/bash_danger.rs` = 1, and that single occurrence is inside the staleness comment on line 87 -- the port has NO substitution-count gate. But `strip_paren_groups` at src/bash_danger.rs:155-166 IS a fixpoint (`loop { let prev = n.clone(); ...; if n == prev { break; } }`), present since the file's first commit.",
"rule": "Two mechanisms x two sides = four cells. A mechanism is present in Claude Code iff its minified source substring matches at least once in the `strings -n 6` output of the version binary; present in the csift port iff the corresponding Rust construct exists in src/bash_danger.rs. Cells observed: fixpoint-strip present in CC = yes, present in port = yes; >64-substitution bail present in CC = yes, present in port = no.",
"note": "Both Claude Code mechanisms hold in 2.1.258 and are not new (the bail message also matches once in the 2.1.229 binary), so nothing drifted on the Claude Code side. Two wording corrections were needed. First, the parenthetical \"(this port is single-pass)\" is false and was false when written: `strip_paren_groups` has been a fixpoint loop since the file's first commit, structurally identical to the binary's `for(let o=\"\";o!==r;)` loop, so the fixpoint is a point of agreement, not a divergence. Second, the >64 bail is narrower than stated: it fires only when the command text also matches `/\\brm(?:dir)?\\b/` (otherwise the pass returns null and analysis continues), and the counted set includes process substitutions and `${ … }` funsubs alongside `$( … )`, so the count in the user-facing message is broader than the phrase \"command substitutions\" suggests. The residual divergence the claim is really about is genuine and confirmed by grep: `64` occurs exactly once in src/bash_danger.rs and only inside the comment, so the port has no count gate and will predict awaiting-execution where Claude Code hoists to explicit approval. The claim's regex-core assumption also re-verified: the binary's `_to` and `yto` are byte-identical to the port's EGP and ZHP in 2.1.258, so the port is not stale in its lexical core."
}
]
}
]
}