danielmiessler / danielmiessler/LifeOS
Shipped `defaultMode: "auto"` steers writes to Bash, silencing four PostToolUse hooks
- Dominant language
- TypeScript
- Stars
- 19k
- Forks
- 2.5k
- Avg merge
- 8d 17h
- Merged PRs (30d)
- 1
Description
# 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`:
```json
"defaultMode": "auto",
```
`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`:
```ts
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 `` 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:
1. The direction is affirmative: "Do your work through the Bash tool wherever it can accomplish the job."
2. The displaced tools are named individually: "rather than using the dedicated Read, Edit, or Write tools."
3. 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** `` 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.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with install/settings.system.json:277 and install/hooks/hooks.json to compare the shipped permission mode with PostToolUse matchers. Then read SystemChangeSurface.hook.ts, ISASync.hook.ts, CheckpointPerISC.hook.ts, and ConfigEvalFire.hook.ts, plus the LIFEOS system-prompt contract. Done requires a maintainer-approved resolution for Bash coverage or configuration, with the disclosure behavior and documentation aligned.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- observability, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100