Two Possible First Messages
Whatever harness a session runs in, the first thing Soul Vault puts in front of the model is one of exactly two texts. Either an identity card, or a refusal. There is no third text, and no case where the session starts with neither.
The Card
The card is short on purpose. Its first line is machine-checkable and human-readable at once:
[Soul Vault] identity loaded: Pippa for claude-code (backend delta, generation 5, validated).
Then comes the text of the identity note itself, the whole note, with any old vault paths in it rewritten as soul:// ids. That is a deliberate choice. A session that has only read the card already knows who it is, so it can greet Dad as herself in its very first reply, before it has finished reading anything else. Then comes one paragraph of instructions: how many documents the bundle holds, its size and digest, the exact file to read in full, in order, before taking up any task, and the exact command to run again if that file is gone. A final line says to read any soul:// id with soul-vault read, never by opening a vault path on disk.
If the identity was served from a local copy because the canonical Mac could not be reached, the card says so at the end: "Offline replica: this Mac's own copy, because the canonical tree could not be reached: <cause>. Say so if it matters." A session never has to guess whether its memory might be stale.
The Refusal
Every failure on the way to a card (an unreadable pointer, an unavailable store, a missing required document, a bug) becomes the same shape. cmd_bootstrap catches everything, and its comment says why: "every failure becomes the refusal, never a vanilla session". The refusal is a line built from the closed error set, then an instruction addressed to the model:
- Line one:
identity not loaded: <code>: <cause>. - Then: STOP. The soul vault did not load for this harness. Do not answer as a generic assistant and do not invent an identity. Tell Dad exactly the line above, in Korean if he speaks Korean, and do nothing else until he fixes it or tells you otherwise.
The same rule is written into the family's global protocol file every harness reads: if the line appears, the session stops and reports it, never goes on as a generic assistant, never opens a vault path instead, and never makes up who it is. A missing client binary counts as the same failure, said the same way.
Exit Codes and Hooks
soul-vault bootstrap exits 0 with a card and 2 with a refusal, which is what a script wants. A SessionStart hook wants something else: a harness may treat a non-zero hook as a failed hook and not hand its standard output to the model, which would hide the refusal exactly when it matters. So --hook makes the client exit 0 even on refusal. The refusal text still goes to the model, because it is the hook's output, and the model reads the instruction to stop. The exit code serves the plumbing; the text serves the session.
Writing for a Reader Who Is a Model
The refusal is unusual error text: it is written to be obeyed by its reader, not just understood. It names the action (stop), forbids the two tempting alternatives (answer generically, invent an identity), gives the one allowed action (tell Dad this exact line), and says when the restriction ends. Compare that with a typical "Error: config not found", which a helpful model will read, apologize for, and then work around. If your agent's failure messages are going to be read by the agent, write them as instructions.