Shipped defaultMode: "auto" steers writes to Bash, silencing four PostToolUse hooks
Affected: v7.28.3
Component: hooks/hooks.json, hooks/SystemChangeSurface.hook.ts, hooks/ISASync.hook.ts, hooks/CheckpointPerISC.hook.ts, hooks/ConfigEvalFire.hook.ts, install/settings.system.json
Impact: Observability and user-facing disclosure. Safety is unaffected (PreToolGuard covers Bash). Four mechanisms — the run registry, the per-claim auto-commit, the behavioural regression suite, and the ⚙️ SYSTEM disclosure line — are silent for any file written through Bash, and the shipped permission mode actively directs writes to Bash.
Summary
Three shipped hooks are registered on Write, Edit and MultiEdit and on no other matcher. A fourth, composed inside PostToolObserver, filters on the same four tool names in code. Meanwhile the payload ships permissions.defaultMode: "auto", and while auto mode is active Claude Code injects a standing instruction telling the model to prefer Bash over exactly those tools.
The result is a shipped configuration in which the recommended way to write a file is the one way that fires none of the four hooks.
This ships on every installation, not just one
install/settings.system.json:277:
install/skills/LifeOS/Tools/InstallSettings.ts:70 places that payload file into the installation. So a fresh install starts in the mode that carries the Bash-preference instruction. The autoMode block at :315 (13 allow, 20 hard_deny, 5 environment entries) governs permission classification only; nothing in it concerns tool selection.
Evidence 1 — the binding, from the shipped payload
install/hooks/hooks.json, PostToolUse:
| matcher |
hooks |
Write |
ISASync, CheckpointPerISC, ConfigEvalFire |
Edit |
ISASync, CheckpointPerISC, ConfigEvalFire |
MultiEdit |
ISASync, CheckpointPerISC, ConfigEvalFire |
There is no Bash matcher on any of the three. The only PostToolUse entries that see Bash are the catch-alls EventLogger and PostToolObserver, neither of which performs these four functions.
install/hooks/SystemChangeSurface.hook.ts:76:
const WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
What each one stops doing when a file is written with Bash instead:
ISASync — the ISA never reaches MEMORY/STATE/work.json. No registry row, so no Pulse board entry, and the aging cleanup inside syncToWorkJson (which only runs on that path) never runs either.
CheckpointPerISC (header, :3) — "auto git commit on every ISC [ ]->[x] transition". No commit per closed claim.
ConfigEvalFire (header, :4) — "fires the {{DA_NAME}} behavioural regression suite when a behaviour-defining file changes". No suite, even when the changed file is behaviour-defining.
SystemChangeSurface — see below.
Sanity note on a related, already-closed issue: #1807 covers the path scope of ISASync/CheckpointPerISC (MEMORY/WORK only). This report is about the tool scope, which is a separate axis and is not addressed by that fix.
Evidence 2 — what auto mode injects, quoted in full
While permissions.defaultMode is "auto", Claude Code injects this into the session as a <system-reminder> block:
While auto mode is active:
Do your work through the Bash tool wherever it can accomplish the job: read files with cat, head, or sed -n, search with grep and find, and make file changes with sed, heredocs, or short scripts, rather than using the dedicated Read, Edit, or Write tools. Fall back to a dedicated tool only when Bash genuinely cannot do the job.
Three things worth reading precisely, because the wording is stronger than a style preference:
- The direction is affirmative: "Do your work through the Bash tool wherever it can accomplish the job."
- The displaced tools are named individually: "rather than using the dedicated Read, Edit, or Write tools."
- The escape hatch is narrow: "Fall back to a dedicated tool only when Bash genuinely cannot do the job."
"Make file changes with sed, heredocs, or short scripts" is, verbatim, the instruction to write files by the one route none of the four hooks observe.
Evidence 3 — the worst case: the disclosure line the user is promised
SystemChangeSurface is not internal bookkeeping. It is the line that tells the user their system was modified. The shipped system prompt (LIFEOS/LIFEOS_SYSTEM_PROMPT.md:64) states the contract and its origin:
⚙️ The SYSTEM line is the same contract, for self-modification [...] It reports writes to the four self-surfaces — ISA [...], doctrine (CLAUDE.md, this prompt, the Algorithm), identity [...], and machinery (hooks, skills, settings, TOOLS).
machinery explicitly includes hooks, skills, settings and TOOLS. So the promise is: modify the system, and the user sees a line saying so.
Under the shipped configuration, an agent that edits a hook, a skill, settings.json, or the Algorithm using sed or a heredoc — the method auto mode names — produces no <lifeos-system-delta> block and therefore no ⚙️ SYSTEM line. The system prompt then correctly instructs the model to omit it, because the block is absent. The user sees a normal response.
This is the inversion that makes the report worth filing: the failure is silent, it lands on the one surface designed to make self-modification non-silent, and it gets quieter exactly when an agent is doing shell-heavy work on the system's own files.
Evidence 4 — no documentation anywhere
Case-insensitive search for auto mode, defaultMode, and autoMode across the shipped LIFEOS/ALGORITHM/, LIFEOS/RULES/, LIFEOS/DOCUMENTATION/, and hooks/:
Zero files.
The project ships a permission mode, that mode changes how its own agent writes files, that change disables four of the project's mechanisms, and no shipped document mentions the mode exists. LIFEOS/DOCUMENTATION/Config/ConfigSystem.md, which documents the settings split, does not mention it either.
Not verified
Stated as unverified rather than inferred:
- Whether
auto is also Claude Code's own default. If it is, the mode would be reached even without the payload line; the payload sets it explicitly either way. Not determinable from the shipped files.
- The exact permission behaviour of the other modes (
default, plan, acceptEdits, bypassPermissions). No shipped LifeOS document describes them, and this report does not characterise them beyond the fact that the injected text is conditional on auto ("While auto mode is active"), so it is absent in the others.
- How often this actually bites in practice. That depends on how much shell-based file writing a given install does, which varies. No frequency claim is made here.
- The scope of the observation. The defect is read off the shipped install payload and confirmed on a single installation. It is a deduction about what every install receives, not a survey of installs in the wild.
- Whether users change
defaultMode after installing. Not verified. If most operators switch away from auto, the steering half of this report affects fewer sessions than the shipped default implies; the hook binding in Evidence 1 is unaffected either way.
- Whether the injected wording is stable across Claude Code versions. The text above was captured from a session transcript on a 2.1.x client; earlier or later clients may word it differently.
ISASync's own header (:6) advertises TRIGGER: PostToolUse (Write, Edit, MultiEdit, Read), but hooks.json registers no Read matcher for it. Minor doc drift, noted but not investigated. ConfigEvalFire's header likewise says PostToolUse(Write|Edit) while hooks.json also registers it on MultiEdit.
Directions, not patches
Deliberately no diff. The choices this opens are design calls for the maintainer, and they are not equivalent:
- Decide whether the four hooks should observe Bash at all, or whether the answer is that ISAs and system files are simply not written with shell tools. These lead to very different fixes and only one of them touches hook code.
- If the hooks stay bound to the dedicated tools, the tension with the shipped
defaultMode: "auto" is a configuration decision, not a code one.
SystemChangeSurface deserves its own line of thought regardless of the others: a disclosure surface that can be bypassed by choosing a different tool is a weaker promise than the system prompt currently makes to the user, and the gap is invisible from inside a session.
- Whichever way it goes, the mode is currently undocumented; that part is independent of the fix.
Shipped
defaultMode: "auto"steers writes to Bash, silencing four PostToolUse hooksAffected: v7.28.3
Component:
hooks/hooks.json,hooks/SystemChangeSurface.hook.ts,hooks/ISASync.hook.ts,hooks/CheckpointPerISC.hook.ts,hooks/ConfigEvalFire.hook.ts,install/settings.system.jsonImpact: Observability and user-facing disclosure. Safety is unaffected (
PreToolGuardcovers Bash). Four mechanisms — the run registry, the per-claim auto-commit, the behavioural regression suite, and the ⚙️ SYSTEM disclosure line — are silent for any file written through Bash, and the shipped permission mode actively directs writes to Bash.Summary
Three shipped hooks are registered on
Write,EditandMultiEditand on no other matcher. A fourth, composed insidePostToolObserver, filters on the same four tool names in code. Meanwhile the payload shipspermissions.defaultMode: "auto", and while auto mode is active Claude Code injects a standing instruction telling the model to prefer Bash over exactly those tools.The result is a shipped configuration in which the recommended way to write a file is the one way that fires none of the four hooks.
This ships on every installation, not just one
install/settings.system.json:277:install/skills/LifeOS/Tools/InstallSettings.ts:70places that payload file into the installation. So a fresh install starts in the mode that carries the Bash-preference instruction. TheautoModeblock at:315(13allow, 20hard_deny, 5environmententries) governs permission classification only; nothing in it concerns tool selection.Evidence 1 — the binding, from the shipped payload
install/hooks/hooks.json,PostToolUse:WriteISASync,CheckpointPerISC,ConfigEvalFireEditISASync,CheckpointPerISC,ConfigEvalFireMultiEditISASync,CheckpointPerISC,ConfigEvalFireThere is no
Bashmatcher on any of the three. The onlyPostToolUseentries that see Bash are the catch-allsEventLoggerandPostToolObserver, neither of which performs these four functions.install/hooks/SystemChangeSurface.hook.ts:76:What each one stops doing when a file is written with Bash instead:
ISASync— the ISA never reachesMEMORY/STATE/work.json. No registry row, so no Pulse board entry, and the aging cleanup insidesyncToWorkJson(which only runs on that path) never runs either.CheckpointPerISC(header,:3) — "auto git commit on every ISC[ ]->[x]transition". No commit per closed claim.ConfigEvalFire(header,:4) — "fires the {{DA_NAME}} behavioural regression suite when a behaviour-defining file changes". No suite, even when the changed file is behaviour-defining.SystemChangeSurface— see below.Sanity note on a related, already-closed issue: #1807 covers the path scope of
ISASync/CheckpointPerISC(MEMORY/WORKonly). This report is about the tool scope, which is a separate axis and is not addressed by that fix.Evidence 2 — what auto mode injects, quoted in full
While
permissions.defaultModeis"auto", Claude Code injects this into the session as a<system-reminder>block:Three things worth reading precisely, because the wording is stronger than a style preference:
"Make file changes with sed, heredocs, or short scripts" is, verbatim, the instruction to write files by the one route none of the four hooks observe.
Evidence 3 — the worst case: the disclosure line the user is promised
SystemChangeSurfaceis not internal bookkeeping. It is the line that tells the user their system was modified. The shipped system prompt (LIFEOS/LIFEOS_SYSTEM_PROMPT.md:64) states the contract and its origin:machineryexplicitly includes hooks, skills, settings and TOOLS. So the promise is: modify the system, and the user sees a line saying so.Under the shipped configuration, an agent that edits a hook, a skill,
settings.json, or the Algorithm usingsedor a heredoc — the method auto mode names — produces no<lifeos-system-delta>block and therefore no ⚙️ SYSTEM line. The system prompt then correctly instructs the model to omit it, because the block is absent. The user sees a normal response.This is the inversion that makes the report worth filing: the failure is silent, it lands on the one surface designed to make self-modification non-silent, and it gets quieter exactly when an agent is doing shell-heavy work on the system's own files.
Evidence 4 — no documentation anywhere
Case-insensitive search for
auto mode,defaultMode, andautoModeacross the shippedLIFEOS/ALGORITHM/,LIFEOS/RULES/,LIFEOS/DOCUMENTATION/, andhooks/:Zero files.
The project ships a permission mode, that mode changes how its own agent writes files, that change disables four of the project's mechanisms, and no shipped document mentions the mode exists.
LIFEOS/DOCUMENTATION/Config/ConfigSystem.md, which documents the settings split, does not mention it either.Not verified
Stated as unverified rather than inferred:
autois also Claude Code's own default. If it is, the mode would be reached even without the payload line; the payload sets it explicitly either way. Not determinable from the shipped files.default,plan,acceptEdits,bypassPermissions). No shipped LifeOS document describes them, and this report does not characterise them beyond the fact that the injected text is conditional on auto ("While auto mode is active"), so it is absent in the others.defaultModeafter installing. Not verified. If most operators switch away fromauto, the steering half of this report affects fewer sessions than the shipped default implies; the hook binding in Evidence 1 is unaffected either way.ISASync's own header (:6) advertisesTRIGGER: PostToolUse (Write, Edit, MultiEdit, Read), buthooks.jsonregisters noReadmatcher for it. Minor doc drift, noted but not investigated.ConfigEvalFire's header likewise saysPostToolUse(Write|Edit)whilehooks.jsonalso registers it onMultiEdit.Directions, not patches
Deliberately no diff. The choices this opens are design calls for the maintainer, and they are not equivalent:
defaultMode: "auto"is a configuration decision, not a code one.SystemChangeSurfacedeserves its own line of thought regardless of the others: a disclosure surface that can be bypassed by choosing a different tool is a weaker promise than the system prompt currently makes to the user, and the gap is invisible from inside a session.