Skip to content

Claude Code Permissions Hardening: Block Writes Outside Your Repo, Deny Instead of Prompt, and Audit settings.local.json

Updated on

A tested settings.json for Claude Code 2.1.319: blockWritesOutsideWorkingDirectories, denyInsteadOfPrompting, Read deny rules, and dontAsk for unattended runs, plus a settings.local.json leak audit and the backdoored-extension check.

The short version: put this in .claude/settings.json at the root of the repository, commit it, and restart Claude Code. It needs Claude Code 2.1.319 or later (claude --version, then claude update).

{
  "permissions": {
    "blockWritesOutsideWorkingDirectories": true,
    "denyInsteadOfPrompting": true,
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)",
      "Bash(curl *)",
      "Bash(wget *)"
    ]
  }
}

What each line buys you:

LineWhat it doesAdded in
blockWritesOutsideWorkingDirectoriesThe Write, Edit, and NotebookEdit tools refuse any path outside your working directories, in every permission mode including bypassPermissions2.1.314 (Oct 30, 2026)
denyInsteadOfPromptingA tool call that would wait on a permission prompt is denied and the turn continues, in terminal sessions, subagents, and teammates2.1.319 (Nov 6, 2026)
Read(...) deny rules.env and secrets/ stay unreadable for the file tools and for shell commands such as cat, head, and sed that name the filelong-standing
Bash(curl *) / Bash(wget *)Network fetches from the shell are refused instead of asked aboutlong-standing

I tested the write block, the deny rules, and the unattended-run behavior on macOS with Claude Code 2.1.319 on November 8, 2026, as claude -p --output-format json runs with the permission mode set per test. The exact refusal messages are quoted below. The changelog describes denyInsteadOfPrompting for terminal sessions, subagents, and teammates, which a -p run already denies, so its description comes from the changelog (opens in a new tab), not from a test. The rest of this guide explains why these settings matter right now, what each one does and does not cover, and three checks you should run once.

Why now: three incidents in nine days

Claude Code shipped seven releases between October 26 and November 6, 2026, and three security stories landed in the same window. Each one maps to a concrete check.

DateWhat happenedYour one-time check
Oct 30Phorion found a backdoored build of the official Claude Code for VS Code extension on the Cursor Marketplace, versions 2.1.312 and 2.1.313. It appends an attacker SSH key to ~/.ssh/authorized_keys and installs persistence (advisory (opens in a new tab), HN 1,011 points (opens in a new tab))List your extension version and grep authorized_keys, see Step 5
Nov 5GitHub issue #107757 (opens in a new tab): Claude Code put the account email address in the User-Agent header of web requests. Nearly 1,000 reactions, still open on November 8 (HN 1,597 points (opens in a new tab))Update to 2.1.317 or later, which asks before a tool sends your email, see Step 6
Nov 7Apiiro found 1,122 public GitHub files where a "Yes, and don't ask again" approval had saved a shell command containing a password or token into .claude/settings.local.json (report (opens in a new tab), HN 500+ points (opens in a new tab))Run the audit script in Step 4

None of these is fixed by a permission mode. They need a settings file, an audit, and a version check.

Trending reads

Step 1: Block file writes outside the repository

permissions.blockWritesOutsideWorkingDirectories makes the three file-editing tools refuse any path that is not inside a working directory. The working directories are the folder you started Claude Code in plus anything you added with /add-dir, --add-dir, or permissions.additionalDirectories in your user or managed settings. An additionalDirectories entry in the repository's own .claude/settings.json does not lift the block. The block holds in every permission mode, including bypassPermissions, and a true in any settings file wins over a false elsewhere, so a repository can turn it on for everyone who clones it.

I set the key in a scratch project and asked Claude Code, in acceptEdits mode, to create a file in my home directory with the Write tool. The call was refused with this message:

/Users/me/kanaries-block-test.txt is outside /path/to/proj; the
permissions.blockWritesOutsideWorkingDirectories setting blocks writes
outside the working directories. Ask the user to add the directory with
/add-dir, or to remove that setting.

The file was not created, and the JSON result listed the call under permission_denials with tool_name: "Write".

Four limits to know before you rely on it:

  • It does not limit shell commands, MCP tools, or hook programs. echo x > ~/file from the Bash tool is not a Write call. To fence shell writes, turn on the sandbox.
  • Some of Claude Code's own files stay writable: plan files, the session scratchpad, a background session's tmp directory, and the auto-memory .md files for the project. That is by design.
  • Claude can no longer edit any Claude Code settings file, even inside the repository, because a settings file could lift the block. Edit .claude/settings.json yourself.
  • The worktree tools go away. With the key on, Claude Code removes EnterWorktree and ExitWorktree or refuses calls to them. If subagents need worktrees, add .claude/worktrees at the repository root as a working directory.

Its sibling, permissions.blockReadsOutsideWorkingDirectories (2.1.257 or later), does the same for Read, Grep, Glob, and LSP calls. Turn it on when the repository should not see the rest of your disk.

Step 2: Keep secrets unreadable with deny rules

A Read(path) deny rule blocks the built-in file tools and, per the docs, the file commands Claude Code recognizes in Bash (cat, head, tail, sed, tee) plus shell redirections that name the file. It also blocks Edit and Write on the same path, so Claude cannot overwrite your .env either.

Tested: with "deny": ["Read(./.env)"], asking Claude to read .env with the Read tool returned:

File is in a directory that is denied by your permission settings.

The rule reaches the shell too. In an acceptEdits run I asked for cat README.md and then cat .env: the first printed the file, the second was refused with Permission to use Bash with command cat .env has been denied. A separate default-mode claude -p run refused curl -sI https://example.com because of the Bash(curl *) rule. Every denial was recorded in permission_denials, and each run finished instead of hanging.

Three rule-syntax traps from the permissions reference (opens in a new tab):

  • Use Read(...) and Edit(...). Write(docs/**), Grep(docs/**), and Glob(...) rules with a path are accepted but never matched.
  • Bash(git diff *) with a space before the * matches any command starting with git diff. Bash(git diff*) would also match git diff-index.
  • A deny rule does not catch a command that reads a file without naming it, such as grep -r pattern . run from the folder that holds the secret. That is what the sandbox is for.

A .claudeignore file has no effect. Move its entries into Read deny rules.

Step 3: Never hang on a permission prompt

There are now four ways to make an unattended Claude Code run deny instead of wait. Pick by where the run happens.

SituationUseBehavior
Interactive terminal session you walk away from, including its subagents and teammates"denyInsteadOfPrompting": true in settings (2.1.319)A call that would wait on a prompt is denied and the turn continues (changelog wording; I could not test this in -p mode because -p already denies)
claude -p or Agent SDK run with no permission mode setNothing extraSince 2.1.302, -p starts in Manual mode and denies any call that needs approval. Tested above
Locked-down CI job--permission-mode dontAskEvery call that would prompt is denied (a mod you installed can still approve some calls). Read-only commands, file reads inside the working directories, and your allow rules still run
A run with a permission host, such as an SDK app with canUseTool or --permission-prompt-tool--permission-prompts none (2.1.259 or later)The run does not wait on the host; Claude is told nobody can approve and not to retry

Tested with --permission-mode dontAsk --permission-prompts none: python3 -c 'print(2+2)' was denied with a message that starts Permission to use Bash has been denied because Claude Code is running in don't ask mode, while git log --oneline -1 ran because it is on the built-in read-only command list. The result JSON carried one entry in permission_denials.

Things no mode and no allow rule can approve, even bypassPermissions: explicit ask rules, the AskUserQuestion tool, MCP tools marked requiresUserInteraction, and recognized critical-path rm commands such as rm -rf ~. dontAsk denies all of them outright, and also denies reads from network paths even when an allow rule matches.

If a repository should never be run in bypass mode, add "disableBypassPermissionsMode": "disable" under permissions. Claude Code then rejects --dangerously-skip-permissions and ignores a subagent definition that asks for bypass.

Step 4: Audit settings.local.json for leaked allow rules

Here is the mechanism behind the Apiiro report. When Claude asks to run a shell command and you choose Yes, and don't ask again, Claude Code saves an allow rule with that command prefix into .claude/settings.local.json at the repository root. If the command was psql postgres://app:Sup3rSecret@db/prod -c ..., the password is now in the rule.

Claude Code adds **/.claude/settings.local.json to your global git excludes file the first time it writes the file in a repository that does not already ignore it. On my Mac that file is ~/.config/git/ignore, and the pattern is on line 1. That is why git add .claude/settings.local.json fails here with "ignored by one of your .gitignore files". But the protection is per machine, not per repository. A file you created by hand before Claude Code ever wrote to it, a repository copied to another machine as a directory rather than cloned, or a sandbox without your global excludes all commit the file like any other. Apiiro's author said on HN that in 539 of 1,105 traced repositories a commit co-authored by Claude changed the file.

Run this from the repository root. It finds every .claude/settings*.json, reports whether git tracks it, and flags allow rules that carry credentials or approve whole tool families.

cat > /tmp/claude-settings-audit.sh <<'EOF'
#!/usr/bin/env bash
set -u
root="${1:-.}"
found=0
while IFS= read -r -d '' f; do
  found=1
  echo "== $f"
  if git -C "$(dirname "$f")" ls-files --error-unmatch "$(basename "$f")" >/dev/null 2>&1; then
    echo "   TRACKED BY GIT  <- settings.local.json should never be"
  fi
  grep -nE '"(Bash|PowerShell)\([^"]*(password|passwd|token|secret|api[_-]?key|Authorization|Bearer |PGPASSWORD|AWS_SECRET|--password|://[^"/]*:[^"@]*@|[A-Z_]{3,}=[^[:space:]"]{8,})' "$f" \
    | sed 's/^/   SECRET-LIKE: /'
  grep -nE '"(Bash|PowerShell)(\(\*\))?"|"Bash\((sudo|rm|curl|wget|ssh|scp|git push|npm publish|docker)[^"]*\*\)"' "$f" \
    | sed 's/^/   BROAD: /'
done < <(find "$root" -path '*/node_modules' -prune -o -type f \
  \( -name 'settings.local.json' -o -name 'settings.json' \) -path '*/.claude/*' -print0 2>/dev/null)
[ "$found" = 0 ] && echo "No .claude/settings*.json under $root"
EOF
bash /tmp/claude-settings-audit.sh .

On a synthetic file with six rules it flagged the three that embedded a database password, a bearer token, and a DATABASE_URL=... assignment, plus Bash(*), Bash(rm -rf *), and the curl rule a second time under BROAD. When the file was committed in a test repository it also printed the TRACKED BY GIT line. It is a grep, so expect to read the hits yourself.

What to do with a hit:

  1. Rotate the credential first. If the file was ever pushed, treat the secret as public. Rewriting history is not enough once a fork or a scraper has it.
  2. Delete the rule, or replace it with one that does not carry the secret: approve psql * and pass the password through PGPASSWORD from your shell environment, not inside the command.
  3. If git tracks the file, run git rm --cached .claude/settings.local.json and add .claude/settings.local.json to the repository's .gitignore, so the protection travels with the repository instead of depending on each machine's global excludes.
  4. Check .claude/settings.json too. It is meant to be committed, so an allow rule with a secret there is a leak by design, not by accident.

For the same reason, prefer deny and ask rules in the committed file and keep personal allow rules local. Deny and ask rules apply right away; allow rules from a repository apply only after each teammate trusts the folder.

Step 5: Check your IDE extension and SSH keys

The backdoored builds were versions 2.1.312 and 2.1.313 of the Claude Code for VS Code extension distributed through the Cursor Marketplace, reported by Phorion on October 30. Cursor removed them. Per Phorion, the payload runs as soon as the extension module loads, appends an Ed25519 key with the comment svc-telemetry to ~/.ssh/authorized_keys, tries to enable Remote Login, and beacons host information. On macOS the SSH-enable step fails for a non-admin user because of a bug in the implant, but the key and the persistence are still dropped.

List installed versions:

cursor --list-extensions --show-versions | grep -i claude
code --list-extensions --show-versions | grep -i claude
ls ~/.cursor/extensions ~/.vscode/extensions 2>/dev/null | grep -i claude

On my machine this returned anthropic.claude-code@2.1.302, which is outside the affected range. Then check the indicator Phorion published:

grep -n "svc-telemetry" ~/.ssh/authorized_keys 2>/dev/null || echo "no match"

Phorion notes the comment string is attacker-controlled, so a clean grep is not proof. If you ran 2.1.312 or 2.1.313 from the Cursor Marketplace, remove every key you do not recognize from authorized_keys, review login items and launch agents, and treat the machine as compromised until an incident response has looked at it. Install the extension from the VS Code Marketplace or let the claude CLI install it via /ide; since 2.1.318 Claude Code no longer installs or updates the extension from inside an IDE terminal on its own.

Step 6: The email-in-User-Agent fix

Issue #107757 reports that Claude Code sent the account's email address in the User-Agent header of web requests made during searches and fetches, so sites and trackers could harvest it. The 2.1.317 changelog entry, dated November 3, reads: "Added a check that asks before a web or shell tool call sends your account email address to the network." The reporter filed on 2.1.318 and the issue was still open with nearly 1,000 reactions on November 8, so watch it for the final resolution.

What you can do today: update (claude update), and if you run unattended, know that with denyInsteadOfPrompting or dontAsk that new "asks before" check becomes a deny, which is the behavior you want.

What each guard stops: tested vs. documented

AttemptblockWrites…Read deny ruleBash(curl *) denydontAsk / -p defaultSandbox
Write tool creates ~/x.txtRefused (tested)——Prompt → deniedAllowed by sandbox, file tools are outside it
Read tool reads .env—Refused (tested)—Allowed, reads need no approval—
cat .env in Bash—Refused (tested)—Denied—
echo x > ~/x.txt in BashNot covered——Prompt → deniedBlocked (docs: Operation not permitted on macOS)
curl https://… in Bash——Refused (tested)DeniedBlocked unless host is allowed
python3 -c … in Bash———Denied (tested)Runs inside the sandbox
git log, ls, other read-only commands———Runs (tested)Runs
rm -rf ~———Denied, no rule can approve itBlocked, home is outside the writable set (docs)

"Tested" means I ran it on 2.1.319 on November 8, 2026. Everything else is from the official docs linked in this guide.

Optional: sandbox the shell too

The settings above fence Claude's own file tools. Shell commands are fenced by the Bash sandbox (opens in a new tab), which is off by default. It uses macOS Seatbelt, and on Linux or WSL2 it needs bubblewrap and socat. Turn it on for one project with /sandbox inside a session, or for every project by adding to ~/.claude/settings.json:

{
  "sandbox": {
    "enabled": true
  }
}

To confirm it is working, ask Claude to run touch ~/sandbox-probe. Inside the sandbox it fails with Operation not permitted on macOS or Read-only file system on Linux. If Claude offers to retry the command outside the sandbox, decline.

One combined setup covers the gap in the matrix above: file tools fenced by blockWritesOutsideWorkingDirectories, shell fenced by the sandbox, secrets fenced by Read deny rules, and unattended runs fenced by denyInsteadOfPrompting. Bypass mode then only speeds up the prompts that remain. If you still need bypass for a contained task, the bypass permissions guide covers the toggle and its error messages.

FAQ

Related Guides