●RELEASE — Claude Code v2.1.246 shipped on August 26, adding a startup warning for Bash allow rules that put a wildcard ahead of the subcommand●PERMISSIONS — /permissions now has an Auto mode tab, so you can see in one place what runs automatically and where Claude still stops to ask●MEMORY — Unbounded memory growth in long interactive sessions is fixed: subagent tool results are released once they scroll out of the recent display window●MCP — In headless and remote sessions, a tool call interrupted by an incoming message is now reported as an explicit interrupted error instead of completing with no output●RUNNER — claude self-hosted-runner gains --proxy-authorization-command and --proxy-authorization-file for egress proxies that issue a fresh auth header on every connection●LIMITS — The 50% weekly limit increase runs through August 31 for Pro, Max, Team, and seat-billed Enterprise accounts, which leaves four days●RELEASE — Claude Code v2.1.246 shipped on August 26, adding a startup warning for Bash allow rules that put a wildcard ahead of the subcommand●PERMISSIONS — /permissions now has an Auto mode tab, so you can see in one place what runs automatically and where Claude still stops to ask●MEMORY — Unbounded memory growth in long interactive sessions is fixed: subagent tool results are released once they scroll out of the recent display window●MCP — In headless and remote sessions, a tool call interrupted by an incoming message is now reported as an explicit interrupted error instead of completing with no output●RUNNER — claude self-hosted-runner gains --proxy-authorization-command and --proxy-authorization-file for egress proxies that issue a fresh auth header on every connection●LIMITS — The 50% weekly limit increase runs through August 31 for Pro, Max, Team, and seat-billed Enterprise accounts, which leaves four days
Trusting the allow rules in your repo, or moving them to the environment
On disposable machines, the permissions.allow rules committed to your repo are dropped while the workspace waits to be trusted. Here is what gets dropped and what survives on 2.1.246, where each kind of rule belongs, and a preflight that catches the gap before a run starts.
I opened the run log one morning and found an unfamiliar line sitting at the very top.
Ignoring 2 permissions.allow entries from .claude/settings.json: this workspace has not been trusted.
The job had not failed. The artifacts were there. But the permission rules I had committed to the repository — both of them — were never read.
As an indie developer I maintain a handful of apps and sites on my own, and the routine parts of that work run every night on a machine that is built from scratch each time. The environment is disposable; the repository is cloned fresh on every run. Putting the permission rules in the repo's .claude/settings.json was supposed to make the behavior identical no matter where it ran. That assumption was wrong.
The conclusion first: on disposable machines, treat permissions.allow in your repo as a nice-to-have. Put what you want blocked in deny, guarantee what you want permitted from the environment side, and detect the gap in a preflight before the run starts. What follows is how I got there, measured on Claude Code 2.1.246.
Which keys actually get dropped
I checked one key at a time. The method is dull: write a .claude/settings.json containing a single key, start one non-interactive session, and count the Ignoring lines printed at startup.
mkdir -p /tmp/probe/.claude && cd /tmp/probeprobe() { printf '%s' "$2" > .claude/settings.json echo "--- $1 ---" claude -p "hi" < /dev/null 2>&1 | grep '^Ignoring ' | sed 's/ Run Claude Code.*//'}probe "allow" '{"permissions":{"allow":["Bash(git *)"]}}'probe "deny" '{"permissions":{"deny":["Bash(rm *)"]}}'probe "ask" '{"permissions":{"ask":["Bash(git push *)"]}}'probe "additionalDirectories" '{"permissions":{"additionalDirectories":["/tmp/other"]}}'probe "hooks" '{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"echo hi"}]}]}}'
Keep the < /dev/null. Without it each probe waits several seconds for stdin, and ten probes turn into a coffee break.
Here is what I got.
Setting
Untrusted workspace
Startup output
permissions.allow
dropped
Ignoring N permissions.allow entries…
permissions.additionalDirectories
dropped
Ignoring N permissions.additionalDirectories entries…
permissions.deny
honored
nothing printed
permissions.ask
honored
nothing printed
hooks / env / statusLine / model
honored
nothing printed
The keys that get dropped are exactly the ones that would be dangerous coming from a stranger. allow says "run this without asking me" and additionalDirectories says "you may touch this too" — adopting either from a repository you have not vetted is a real risk. deny and ask only make things stricter, so there is no reason to discard them.
The design is coherent. But if you have not internalized the asymmetry, and you assume permissions simply work when placed in settings.json, what you actually get is a state where only the loosening half of your configuration has quietly vanished.
One detail for parsing: each dropped key gets its own line, and the wording switches between entry and entries depending on the count. Counting lines is more reliable than matching the number.
It will never become trusted on its own
My first assumption was that the warning would appear once and then sort itself out after a few runs. It does not.
Run once in an untrusted directory, then look at the user config:
The output was {}. An untrusted non-interactive run does not even create an entry for the directory. It is not recorded with the trust flag set to false — there is no record at all.
Nothing moves forward on its own. Every nightly run drops the same rules at the same point, forever. And because the artifacts still appear, you will not notice unless you read back to the first line of the log.
That is the uncomfortable part. A broken configuration would stop; this one completes successfully with one capability quietly removed. It rhymes with the way a typo in a settings key is ignored without a word (A One-Letter Typo in settings.json Is Ignored Without a Single Warning). The difference is that this failure does leave you one line of evidence. Whether you can pick that line up mechanically is what decides the outcome.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to tell whether the permission rules in your repo are actually in effect, from a single startup's worth of output
✦You will be able to stop a disposable environment from running for days with its allow rules silently discarded, by catching it in a preflight
✦You will be able to decide where each rule belongs, knowing which side of the permission config survives an untrusted workspace
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Trust is recorded in the user config at ~/.claude.json, under projects, keyed by the absolute path of the working directory. Two things cost me time here.
A trailing slash does not match. Writing trust under projects["/tmp/probe/"] left a session started in /tmp/probe untrusted. The key is not normalized; it appears to be compared as a plain string. When writing it from a script, run the path through cd "$dir" && pwd -P first so trailing slashes and symlinks are gone.
Trusting a parent does not cover its children. With /tmp/probe trusted, starting in /tmp/probe/sub still dropped the allow rules from sub/.claude/settings.json. If you switch working directories per package in a monorepo, registering the root is not enough.
Trust entry
Started in
Result
projects["/tmp/probe"] = true
/tmp/probe
allow honored
projects["/tmp/probe/"] = true (trailing slash)
/tmp/probe
dropped
projects["/tmp/probe"] = true
/tmp/probe/sub
dropped
If your clone path changes from run to run, that strictness becomes an operational cost. Either pin the path, or register it in a preflight every time.
The decision: deny in the repo, allow in the environment
With that settled, I moved things around.
What I want blocked lives in the repository.deny and ask are read regardless of trust, so committing them means every clone, on every machine, is at least as strict as intended. Repository-specific promises — never read production credentials here, always confirm before pushing — belong in that half. The fact that they cannot be dropped is precisely what makes them worth relying on.
What I want permitted lives in the environment. I now treat allow as a speed optimization that only applies in a trusted environment. It goes into user-scope settings, or gets written alongside trust registration when the machine is provisioned. I kept the repo-side allow as a convenience for working by hand on my own laptop, but nothing unattended depends on it anymore.
Reduced to one line: do not put settings that fail dangerously in a place where they can fail. An allow that disappears is not dangerous — you just get more confirmations. A deny that disappears would be dangerous, and it does not disappear. Given that, letting the repository carry the safety declarations and the environment carry the convenience ones is simply the shape the tool already has.
There is a side benefit. Long allow lists cost you on every turn (The Permission Rules You Added for Safety Are Taxing Every Turn), and once the split is in place, the allow list you ship to an unattended machine only needs the commands that specific job actually runs.
A preflight that stops the run first
I added one small script to the provisioning side. It does two things: register the working directory as trusted, then start a session and confirm no Ignoring line appears. If one does, it stops there.
#!/usr/bin/env bash# trust-preflight.sh — confirm project settings are actually read before an unattended runset -euo pipefailREPO_DIR="$(cd "${1:?usage: trust-preflight.sh <repo-dir>}" && pwd -P)" # strip trailing slash and symlinksCONFIG="${CLAUDE_CONFIG_DIR:-$HOME}/.claude.json"python3 - "$CONFIG" "$REPO_DIR" << 'PY'import json, os, sys, tempfileconfig_path, repo_dir = sys.argv[1], sys.argv[2]data = {}if os.path.exists(config_path): with open(config_path) as f: try: data = json.load(f) except json.JSONDecodeError: # overwriting a corrupted config makes recovery painful, so bail out untouched print(f"config is not valid JSON: {config_path}", file=sys.stderr) sys.exit(2)projects = data.setdefault("projects", {})entry = projects.setdefault(repo_dir, {})if entry.get("hasTrustDialogAccepted") is True: print("already trusted") sys.exit(0)entry["hasTrustDialogAccepted"] = True# write to a temp file on the same filesystem, then swap it in,# so a crash mid-write cannot leave the config half-writtenfd, tmp = tempfile.mkstemp(dir=os.path.dirname(config_path) or ".")with os.fdopen(fd, "w") as f: json.dump(data, f)os.replace(tmp, config_path)os.chmod(config_path, 0o600)print("trust added")PYcd "$REPO_DIR"DROPPED="$(claude -p "reply with: ok" < /dev/null 2>&1 | grep -c '^Ignoring ' || true)"if [ "$DROPPED" -ne 0 ]; then echo "PREFLIGHT FAILED: ${DROPPED} settings entries were ignored at startup" >&2 exit 1fiecho "PREFLIGHT OK: $REPO_DIR"
What I observed running it: against an untrusted directory it prints trust added and then PREFLIGHT OK; a second run prints already trusted and succeeds the same way. Passing the path with a trailing slash still produced exactly one key, without the slash. Clearing the trust entry and starting plainly again brought back two Ignoring lines. A check whose failure path you have never exercised is not a check.
A few deliberate choices in there.
os.replace is used because ~/.claude.json holds state beyond trust, and corrupting it mid-write turns a two-minute task into an afternoon. For the same reason, a config that will not parse is left alone and the script exits with status 2.
chmod 600 is explicit because tempfile.mkstemp creates the file with its own permissions, and replacing the config does not inherit the original mode. It has to be set on every swap.
grep -c is wrapped in || true because grep exits 1 when it matches nothing. Under set -e, omitting that makes the script fail exactly when the warning did not appear — success reported as failure, which is the hardest kind of bug to notice.
Or just keep the detection
Some environments will not let the runner write trust, and in others you would rather it never happen automatically. Detection alone is still worth having.
The leading ^ matters: without it, the word Ignoring appearing anywhere in the model's reply inflates the count. The narrower the output you assert on, the longer the check survives.
I put this as the last step of provisioning. It costs one startup. Weighed against a nightly job running for weeks with no permission rules at all, that is cheap.
It is worth pointing the same skepticism at the other half. deny survives an untrusted workspace, but surviving is not the same as covering what you think it covers — I once had read-deny rules that never reached inside a folder I had allowed (My read-deny rules never reached inside the folder I had allowed).
Start by reading the first line of your last unattended run
One thing to do: open the most recent log from a scheduled run and look for a line starting with Ignoring. If it is there, that environment has been running without its permission rules.
It took me a while to catch. The output was correct, the artifacts were complete, and there was nothing to be suspicious of. The only way I know to find something that quietly runs at reduced capability is to have a machine looking while it is still quiet. Thank you for reading — I am still refining my own setup here, and I hope this saves you the morning I spent on it.
(Verified on Claude Code 2.1.246, Linux, non-interactive mode)
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.