Arising from dec-0003 — No model client in the binary — semantic extraction is delegated to the live session
The tier-2 handoff cannot ride PreCompact stdout, which reaches only the compaction summariser
`dira sniff --deep --stage` runs from a `PreCompact` hook. The design assumed the block it prints reaches the live Claude session, which would then fill in the because, the alternatives and the `derives_from` edge and call `dira log`. That assumption is false, and this entry records what is true instead so the next person does not re-derive it.
What the hook contract actually says
**Which events inject context.** From the hooks reference (<https://code.claude.com/docs/en/hooks>), on exit code 0:
> For most events, stdout is written to the debug log but not shown in the > transcript. The exceptions are `UserPromptSubmit`, `UserPromptExpansion`, and > `SessionStart`, where stdout is added as context that Claude can see and act on.
`PreCompact` is not among the exceptions.
**Whether a JSON additional-context field is honoured there.** The same page's decision-control table puts `PreCompact` in the `decision`-only row:
> | UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, > PostToolBatch, Stop, SubagentStop, ConfigChange, PreCompact | Top-level > `decision` | `decision: "block"`, `reason`. Stop and SubagentStop also accept > `hookSpecificOutput.additionalContext` for non-error feedback that continues > the conversation |
The shipped binary agrees. `~/.local/share/claude/versions/2.1.220` carries the hook output schema's own per-event descriptions, and the `hookSpecificOutput` union enumerates variants for `SessionStart`, `UserPromptSubmit`, `UserPromptExpansion`, `PreToolUse`, `PostToolUse`, `PostToolBatch`, `PostToolUseFailure`, `SubagentStart`, `Stop`, `SubagentStop`, `SessionEnd`, `Notification`, `PermissionRequest`, `PermissionDenied`, `CwdChanged`, `FileChanged`, `MessageDisplay`, `Elicitation`, `ElicitationResult` and `WorktreeCreate`. **There is no `PreCompact` variant.** The `Stop` one reads:
> Hook-specific output for the Stop event. additionalContext is non-error > feedback delivered to the model; the conversation continues so the model can > act on it.
**Where `PreCompact` stdout actually goes — and this is the part the documentation does not spell out.** The binary's own hook help says:
> Before conversation compaction > Input to command is JSON with compaction details. > Exit code 0 - stdout appended as custom compact instructions > Exit code 2 - block compaction > Other exit codes - show stderr to user only but continue with compaction
That is confirmed in the code path rather than only in help text. The `PreCompact` hook runner returns `{blockedBy, newCustomInstructions}`, and `newCustomInstructions` is passed straight into the summarisation call as its `customInstructions`. That call runs with `querySource: "compact"`, `maxTurns: 1`, and a prompt ending:
> REMINDER: Do NOT call any tools. Respond with plain text only — an > `<analysis>` block followed by a `<summary>` block. Tool calls will be > rejected and you will fail the task.
So the honest answer to "where does `PreCompact` stdout go" is neither "the model" nor "discarded": it goes to **the compaction summariser**, a one-turn call that is explicitly forbidden from calling tools. A handoff block delivered there can influence what the summary preserves. It can never produce a `dira log` invocation.
What was wrong in the repository, stated rather than quietly patched
`hooks/settings.example.json`'s `PreCompact` comment said `--deep` "asks the live session (via the skill) to fill in the because, the rejected alternatives, and the derives_from edge". It cannot, and never could. The wiring has been claiming a capability the hook contract does not provide. That comment is corrected in the same change as this entry, and now names this entry so the claim carries its evidence.
Note the failure mode this had: `dira sniff --deep --stage 2>/dev/null || true` exits 0, the regex tier stages its candidates, and nothing about the run looks wrong. A capability that silently does not exist is worse than one that is missing, because nobody goes looking.
The seams that do carry it
Two, both already precedented in this repository's own hook config:
- **`Stop`** accepts `hookSpecificOutput.additionalContext`, delivered to the model with the conversation continuing so it can act on it. `dira sniff` already runs on `Stop`. This is the near seam. - **`SessionStart`** injects stdout as context, and its matcher values are `startup`, `resume`, `clear`, `compact`, `fork` — so a `SessionStart` hook fires after a compaction with its output delivered. `dira brief --context` already runs there, which is a working precedent to read rather than invent.
Neither is a drop-in for `PreCompact`'s timing. `PreCompact` is the only point at which the about-to-be-lost transcript still exists, so the *capture* stays there; what moves is where the *handoff* is delivered, and it arrives one beat later than the design assumed.
What this does not settle
Which of the two seams E2-L2 should build on, and what that does to the shape of the handoff block, are decisions for the tasks that build it — this entry establishes the contract they have to design against, not the design. The `acc:` clause requiring `--deep` to emit a block on stdout is still mechanically true and still worth having: the block is the artifact, and it can be delivered from more than one place. What is now false, and must not be re-asserted anywhere, is that emitting it on `PreCompact` stdout causes the live session to act on it.
Alternatives on record — 3
✗Leave the handoff on PreCompact stdout and let it ride into the compaction summaryrefused PreCompact stdout on exit 0 is not discarded — it is appended to the compaction summariser's prompt as custom instructions.
But that summariser is run with maxTurns 1 and is told, in the prompt itself, "Do NOT call any tools ... Tool calls will be rejected and you will fail the task." So the only reader of the handoff is a model that structurally cannot call `dira log`. At best the block's text survives into the summary, which is a strictly weaker guarantee than the one hooks/settings.example.json claims, and it fails silently: everything exits 0 and no entry is ever written.
✗Correct the comment in hooks/settings.example.json and leave `--deep` with no delivery seamrefused It stops the false claim spreading, which is why the comment is corrected here regardless.
But it is not a resting place: dec-0022 routes every confirmed regex-staged decision into the semantic tier, so a tier with no delivery seam means confirmed entries pile up in `staged` waiting for an extraction that has no way to be asked for.
✗Move the whole capture to SessionStart with the `compact` matcher and drop the PreCompact hookrefused SessionStart does inject context and its matcher list includes `compact`, so it genuinely fires after a compaction with its output delivered to the model.
But it fires AFTER — the pre-compaction transcript the regex tier reads is already gone by then. PreCompact is the only point at which that transcript still exists, so the capture must stay there even though the handoff cannot. What moves is the delivery, not the capture.