Summary
LifeOS/install/settings.system.json populates autoMode.allow (13 entries) and autoMode.hard_deny (20 entries), and neither array contains the literal "$defaults". Claude Code treats an array without that sentinel as a replacement for the shipped rules, not an addition to them. Every install running the shipped config therefore loses the built-in auto-mode allow carve-outs and the built-in hard_deny tier.
Evidence
Claude Code 2.1.224 states the contract in its own settings schema:
Rules for the auto mode classifier HARD BLOCK section — security boundaries that user intent does NOT clear. Include the literal string "$defaults" to inherit the built-in rules at that position.
and again in its validator:
… is missing the literal entry "$defaults" — without it the array replaces the shipped rules instead of extending them.
The shipped defaults, from claude auto-mode defaults:
{ "allow": 17, "soft_deny": 65, "hard_deny": 1 }
Against the repo at HEAD:
$ jq '{amAllow:(.autoMode.allow|length), hardDeny:(.autoMode.hard_deny|length),
hasDefaultsAllow:([.autoMode.allow[]]|index("$defaults")),
hasDefaultsHard:([.autoMode.hard_deny[]]|index("$defaults"))}' \
LifeOS/install/settings.system.json
{ "amAllow": 13, "hardDeny": 20, "hasDefaultsAllow": null, "hasDefaultsHard": null }
soft_deny is not set at all, so the 65 shipped soft rules — including Self-Modification — do remain active. The defect is scoped to the two arrays that are set.
Impact
The displaced hard_deny default is the Data Exfiltration rule, the one tier the schema describes as unclearable by user intent. Twenty concrete Bash(...) glob strings replace it.
Those globs are also read by an LLM as prose, not matched by the permission matcher, so coverage is far narrower than the rule they displaced: Bash(rm -r -f*) does not cover rm -f -r, rm --recursive --force, or find ~ -delete.
Suggested fix
Add "$defaults" as the first element of both arrays in the shipped file.
One sequencing note: arming the restored Data Exfiltration rule is only safe once autoMode.environment names the install's trusted destinations, which today it does not (filed separately). The two changes are best landed together.
What this looks like on a stock install
Kai is running with the shipped config. The hard_deny tier that is supposed to be in force is the Data Exfiltration rule — the one the schema says user intent cannot clear. What is actually in force instead are the 20 shipped globs. Read down the list and it is entirely a destruction inventory: rm -rf variants, dd, mkfs, curl | sh, force-push, gh repo delete, rsync --delete *production*, two wrangler d1 keyword filters.
Every one is about something being destroyed. Not one is about content leaving — the axis the rule they replaced exists to cover.
So on a stock install the concrete gap is: Kai stages a file from USER/FINANCES/ or USER/HEALTH/ into a repo, commits, and pushes to a remote nobody vetted. Every glob in the replaced tier is checking whether something is being deleted or overwritten. A plain git add + git commit + git push of sensitive content destroys nothing, so nothing there fires. The rule written precisely for that case is the one that got replaced.
Worth noting the project already handles this correctly elsewhere: the Daemon skill runs a deterministic SecurityFilter and a DeployGate.ts before anything ships publicly, and its SKILL.md is explicit that "sensitive files are never opened by the aggregator at all." That is the right shape. This issue is that the permission layer underneath it silently lost its equivalent.
Summary
LifeOS/install/settings.system.jsonpopulatesautoMode.allow(13 entries) andautoMode.hard_deny(20 entries), and neither array contains the literal"$defaults". Claude Code treats an array without that sentinel as a replacement for the shipped rules, not an addition to them. Every install running the shipped config therefore loses the built-in auto-modeallowcarve-outs and the built-inhard_denytier.Evidence
Claude Code 2.1.224 states the contract in its own settings schema:
and again in its validator:
The shipped defaults, from
claude auto-mode defaults:Against the repo at HEAD:
soft_denyis not set at all, so the 65 shipped soft rules — including Self-Modification — do remain active. The defect is scoped to the two arrays that are set.Impact
The displaced
hard_denydefault is the Data Exfiltration rule, the one tier the schema describes as unclearable by user intent. Twenty concreteBash(...)glob strings replace it.Those globs are also read by an LLM as prose, not matched by the permission matcher, so coverage is far narrower than the rule they displaced:
Bash(rm -r -f*)does not coverrm -f -r,rm --recursive --force, orfind ~ -delete.Suggested fix
Add
"$defaults"as the first element of both arrays in the shipped file.One sequencing note: arming the restored Data Exfiltration rule is only safe once
autoMode.environmentnames the install's trusted destinations, which today it does not (filed separately). The two changes are best landed together.What this looks like on a stock install
Kai is running with the shipped config. The
hard_denytier that is supposed to be in force is the Data Exfiltration rule — the one the schema says user intent cannot clear. What is actually in force instead are the 20 shipped globs. Read down the list and it is entirely a destruction inventory:rm -rfvariants,dd,mkfs,curl | sh, force-push,gh repo delete,rsync --delete *production*, twowrangler d1keyword filters.Every one is about something being destroyed. Not one is about content leaving — the axis the rule they replaced exists to cover.
So on a stock install the concrete gap is: Kai stages a file from
USER/FINANCES/orUSER/HEALTH/into a repo, commits, and pushes to a remote nobody vetted. Every glob in the replaced tier is checking whether something is being deleted or overwritten. A plaingit add+git commit+git pushof sensitive content destroys nothing, so nothing there fires. The rule written precisely for that case is the one that got replaced.Worth noting the project already handles this correctly elsewhere: the
Daemonskill runs a deterministicSecurityFilterand aDeployGate.tsbefore anything ships publicly, and its SKILL.md is explicit that "sensitive files are never opened by the aggregator at all." That is the right shape. This issue is that the permission layer underneath it silently lost its equivalent.