feat(settings): rebuild destructive-command cover in autoMode, scope the classifier environment
`permissions.ask` gates nothing under `defaultMode: auto` (LRN-146, verified live), so the ten rules that left the static tiers had no cover left: rsync / kill -9 / killall / pkill out of deny, and python3 -c / python -c / xargs / sed / cp / mv out of ask. autoMode.soft_deny (7 rules) takes over what an explicit instruction should be able to clear: writes outside the working directory, rsync --delete, SIGKILL and kill-by-name, in-place edits spanning more than one file, directory moves, and inline interpreters or xargs that delete or write outside the cwd. Intent clears a soft block for the current turn only, stated as a rule since no setting expresses it. autoMode.hard_deny (3 rules) takes the classes no command pattern can express: secret exfiltration, production deployment, and disarming the guardrails. Adding a restriction stays allowed, removing one does not. permissions.deny gains ten .env reader rules (sed awk cut tr sort uniq diff od xxd strings). Six of those tools sat in permissions.allow, so reading a .env through them triggered nothing. autoMode.environment named another project, its FTP deploy target and its customer data, inside the file link.sh:21 symlinks to ~/.claude/settings.json, where it reached every repo and contradicted this one's Gitea remote. Rewritten machine-generic; the project facts moved to that project's gitignored .claude/settings.local.json. All three lists now open with "$defaults", which the original omitted, so the built-in classifier entries are inherited rather than replaced. doctor.sh check_automode backstops both defects. SETTINGS.md documents the block and a tier-choice table. README no longer claims the ask tier makes every mcp__magic__* call require a live confirmation.
This commit is contained in:
@@ -46,6 +46,66 @@ Always write the file-write ban as `Edit(...)`.
|
||||
| `auto` | Research preview — agentic default, permission model evolving. This config's default (BDR-004) | Daily driving with guardrails |
|
||||
| `bypassPermissions` | Skips all prompts — **dangerous** | CI/CD only, sandboxed env |
|
||||
|
||||
## Auto mode (`autoMode`)
|
||||
|
||||
With `defaultMode: auto`, a classifier decides each action instead of a static
|
||||
prompt. The `autoMode` block is what you hand that classifier.
|
||||
|
||||
| Key | What it holds |
|
||||
|---|---|
|
||||
| `environment` | Facts about the machine and the repo. Context, not rules. |
|
||||
| `allow` | Action classes the classifier may clear on its own. |
|
||||
| `soft_deny` | Destructive or irreversible actions. Explicit user intent clears them. |
|
||||
| `hard_deny` | Security boundaries. User intent does **not** clear them. |
|
||||
| `classifyAllShell` | `true` suspends every Bash allow rule so all shell goes through the classifier. |
|
||||
|
||||
All four lists are prose spliced into the classifier prompt, not permission-rule
|
||||
syntax. Write `Sending SIGKILL reaches processes outside this session`, not
|
||||
`Bash(kill -9 *)`.
|
||||
|
||||
### `$defaults`
|
||||
|
||||
Each list **replaces** the built-in entries unless it contains the literal
|
||||
string `"$defaults"`, which splices them in at that position. Put it first and
|
||||
your own entries refine what follows. Omit it and you silently drop every
|
||||
built-in rule, which is almost never the intent.
|
||||
|
||||
### Scope it right
|
||||
|
||||
`autoMode` in `~/.claude/settings.json` reaches **every** project on the
|
||||
machine. Project facts (this repo's deploy target, its secrets, its data)
|
||||
belong in that project's `.claude/settings.local.json`. A global block naming
|
||||
one repo feeds the classifier false facts in all the others.
|
||||
|
||||
### `ask` is not a prompt under auto mode
|
||||
|
||||
Verified in-session (LRN-146): with `defaultMode: auto`, Bash rules in
|
||||
`permissions.ask` were auto-approved and raised no prompt. `deny` is the only
|
||||
tier the classifier cannot lift.
|
||||
|
||||
So for a destructive command you want gated but still reachable, `ask` is the
|
||||
wrong tier. Use `autoMode.soft_deny`: blocked until the user's intent clears
|
||||
it. Keep `deny` for what must never run at all.
|
||||
|
||||
### Picking a tier
|
||||
|
||||
| You want | Tier |
|
||||
|---|---|
|
||||
| Never runs, no exception, matchable by a command pattern | `permissions.deny` |
|
||||
| Never runs, and a pattern cannot express it (a read then a send, a prod target) | `autoMode.hard_deny` |
|
||||
| Runs when the user asks for it, blocked otherwise | `autoMode.soft_deny` |
|
||||
| Runs freely | `permissions.allow`, or nothing |
|
||||
|
||||
`permissions.ask` is not on this list on purpose. Under `defaultMode: auto` it
|
||||
gates nothing.
|
||||
|
||||
### Scope of intent
|
||||
|
||||
A `soft_deny` clears on the user's instruction, and this config scopes that to
|
||||
the **current turn**. An approval from an earlier turn is not an approval now.
|
||||
State the scope in the rules themselves: the classifier reads the list, it has
|
||||
no separate setting for this.
|
||||
|
||||
## Security notes
|
||||
|
||||
- `Read(**/.env)` only blocks the Read tool. `Bash(cat .env)` bypasses it unless separately denied.
|
||||
@@ -53,6 +113,8 @@ Always write the file-write ban as `Edit(...)`.
|
||||
- `disableBypassPermissionsMode: "disable"` prevents switching to bypass mode mid-session.
|
||||
- Prefer `ask` over `allow` for anything touching external systems.
|
||||
- `deny` in `~/.claude/settings.json` cannot be overridden by project-level `allow` — deny always wins.
|
||||
- Under `defaultMode: auto`, `ask` does not raise a prompt (see above). A destructive
|
||||
command belongs in `deny` or in `autoMode.soft_deny`, not in `ask`.
|
||||
|
||||
## managed-settings.json (enterprise)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user