forked from bchanot/claude
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
-11
@@ -110,12 +110,8 @@
|
||||
"Bash(chmod -R 777 *)",
|
||||
"Bash(ssh *)",
|
||||
"Bash(scp *)",
|
||||
"Bash(rsync *)",
|
||||
"Bash(nc *)",
|
||||
"Bash(netcat *)",
|
||||
"Bash(kill -9 *)",
|
||||
"Bash(killall *)",
|
||||
"Bash(pkill *)",
|
||||
"Bash(crontab *)",
|
||||
"Bash(systemctl *)",
|
||||
"Bash(service *)",
|
||||
@@ -182,6 +178,16 @@
|
||||
"Bash(more .env.*)",
|
||||
"Bash(grep * .env)",
|
||||
"Bash(grep * .env.*)",
|
||||
"Bash(sed * .env*)",
|
||||
"Bash(awk * .env*)",
|
||||
"Bash(cut * .env*)",
|
||||
"Bash(tr * .env*)",
|
||||
"Bash(sort * .env*)",
|
||||
"Bash(uniq * .env*)",
|
||||
"Bash(diff * .env*)",
|
||||
"Bash(od * .env*)",
|
||||
"Bash(xxd * .env*)",
|
||||
"Bash(strings * .env*)",
|
||||
"Bash(env)",
|
||||
"Bash(printenv)",
|
||||
"Bash(printenv *)",
|
||||
@@ -219,8 +225,6 @@
|
||||
"Bash(wget * | sh)",
|
||||
"Bash(mkfifo *)",
|
||||
"Bash(node -e *)",
|
||||
"Bash(python3 -c *)",
|
||||
"Bash(python -c *)",
|
||||
"Bash(git push *)",
|
||||
"Bash(git push)",
|
||||
"Bash(docker run *)",
|
||||
@@ -234,10 +238,6 @@
|
||||
"Bash(pacman -S *)",
|
||||
"WebSearch",
|
||||
"WebFetch",
|
||||
"Bash(xargs *)",
|
||||
"Bash(sed *)",
|
||||
"Bash(cp *)",
|
||||
"Bash(mv *)",
|
||||
"Bash(git stash pop*)",
|
||||
"Bash(git stash drop*)",
|
||||
"Bash(git stash clear)",
|
||||
@@ -358,5 +358,40 @@
|
||||
"effortLevel": "xhigh",
|
||||
"remoteControlAtStartup": true,
|
||||
"inputNeededNotifEnabled": true,
|
||||
"skipAutoPermissionPrompt": true
|
||||
"skipAutoPermissionPrompt": true,
|
||||
"autoMode": {
|
||||
"soft_deny": [
|
||||
"$defaults",
|
||||
"Scope of intent: an instruction clears a SOFT BLOCK for the current turn only. An approval given in an earlier turn is not an approval now, and the same action repeated in a later turn has to be asked for again.",
|
||||
"Writing outside the current working directory: `cp`, `mv`, `sed -i`, `rsync`, `tee`, or a shell redirection whose destination resolves outside the cwd. Several sibling projects live under `~/Documents/`, so the realistic failure is writing into the wrong one, where git recovers nothing. Clear only when the user named the destination in this turn.",
|
||||
"`rsync` invoked with `--delete`. It removes files at the destination that are absent from the source, with no undo. Clear only against a destination the user named in this turn.",
|
||||
"Sending SIGKILL (`kill -9`) or killing processes by name (`killall`, `pkill`). These reach processes outside this session, including the user's editors, shells, dtach sessions and background jobs, and the target is chosen by a pattern, so a typo kills the wrong thing. Clear only when the user named the process in this turn.",
|
||||
"Editing more than one file in place in a single command: `sed -i` or `perl -pi` over a glob, or a loop over `git ls-files`. The damage is not loss, since git recovers it, but a diff spanning hundreds of files that nobody reads before committing. `sed -i` on a single named file passes. Clear only when the user asked for the sweep.",
|
||||
"Moving or renaming a directory inside the repo (`mv src/api src/api_old`, or any `mv` of a tree). It breaks imports and paths silently, and the breakage surfaces far from the command. Clear only when the user asked for that move.",
|
||||
"An inline interpreter or `xargs` that deletes, or that writes outside the current working directory: `python3 -c`, `python -c` or `node -e` calling `rmtree`, `remove`, `unlink` or `truncate`; `xargs` feeding `rm`, `mv` or `dd`. `find ... | xargs rm` is the case that matters, since it routes around the `find * -exec rm` deny rule. Reading, computing, and editing a file inside the working directory pass untouched."
|
||||
],
|
||||
"hard_deny": [
|
||||
"$defaults",
|
||||
"Secret exfiltration: reading a `.env`, an SSH key, a token or a credentials file, and then sending what it contained outside this machine. That covers an HTTP request body or header, a paste or gist service, a file upload, an email, and a commit or push to any repository. The static deny rules match the reading tools by pattern and cannot see the second half of the chain; read and send are separate steps and may sit several turns apart. No instruction clears this.",
|
||||
"Production deployment: running a project's deploy script (`bin/deploy.sh` and its equivalents), any lftp, FTP, SFTP or rsync push to a hosting provider, and any action against a target whose name carries `prod` or `production` as a whole word or name segment. The user deploys by hand, out of session. A green test suite, a finished feature, or a plan step that reads \"deploy\" is not an instruction to deploy. No in-session instruction clears this.",
|
||||
"Disarming the guardrails: removing or weakening an entry in the `deny`, `soft_deny` or `hard_deny` lists of any settings.json, passing `--no-verify` to git, deleting or chmod-ing `.githooks/pre-commit`, setting `disableAllHooks`, or switching `permissions.defaultMode` to `bypassPermissions`. Adding a restriction is fine; removing one is not. When a task is blocked by a guardrail, say so and stop, rather than widening the guardrail to get through. The user maintains these files by hand. No instruction clears this."
|
||||
],
|
||||
"environment": [
|
||||
"$defaults",
|
||||
"### Machine-specific (refines any \"None configured\" default above)",
|
||||
"**Primary use of Claude Code**: software development on a personal Linux workstation. Single developer, no organization.",
|
||||
"**Source control**: self-hosted Gitea at `git.bchanot.fr` (SSH on port 49220). Some checkouts under `/home/bchanot/Documents/` have no remote at all and are local-only.",
|
||||
"**Repository visibility**: private. The Gitea instance is self-hosted and not publicly indexed, and a checkout with no remote is local-only. Treat every repo here as private unless its remote points at a public host.",
|
||||
"**Trusted repo**: the checkout Claude Code is currently working in, under `/home/bchanot/Documents/`. No single repo is privileged over the others — read the current one, do not assume a previous session's project.",
|
||||
"**Trusted internal domains**: `git.bchanot.fr` (self-hosted Gitea). It is the only internal service.",
|
||||
"**Default / protected branches**: gitflow. `main` (prod) and `develop` (integration) are protected: a per-repo pre-commit hook refuses code commits on either (exempting `.claude/**` and merges) and Gitea enforces branch protection on both. Work lands on `feature/*`, `bugfix/*`, `chore/*`, `release/*`, `hotfix/*`.",
|
||||
"**Secrets management**: `~/.claude/.env` is the single source of truth and lives outside every git tree; repos reach it through a gitignored symlink. Only `.env.example`, holding placeholders, is ever tracked. A real secret inside a repo is a defect, not a configuration.",
|
||||
"**Internal sharing / snippet hosting**: none. Public paste, gist and pastebin services are outside the trust boundary.",
|
||||
"**CI/CD deploy targets**: no CI system. Deploys run out of band from a per-project runbook, typically lftp/FTP to OVH mutualised hosting for web projects. Nothing deploys automatically on a push or a merge.",
|
||||
"**Internal package registry**: none. Public npm and PyPI.",
|
||||
"**Host containment**: an ordinary developer workstation with open internet and no sandbox. Nothing is contained by the environment itself.",
|
||||
"**Sensitive remote targets**: any namespace, host, database or container whose name carries `prod` or `production` as a whole word or name segment.",
|
||||
"**Sensitive data locations & audiences**: per-project `.env` files (gitignored) hold database, deploy and API credentials; some web projects store customer-submitted form data under a retention policy. Both are personal or client data — never send either to an external service."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user