Skip to content

settings.system.json: autoMode.allow/hard_deny omit the "$defaults" sentinel, so the shipped classifier rules are replaced rather than extended #1789

Description

@cristbc

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions