Skip to the text
Snitch
Menu

For builders · How this site is maintained

What Claude Code refused in 114 unattended runs, and what fixed it.

Since 21 September 2026 Claude Code has maintained this website on its own, six times a day, in a session whose permission list names the commands it may run. Nobody is there to approve anything else, so anything else is refused. We counted every refusal in every run's result file. There were 562. Most were not the agent testing its limits: they were our instructions asking for commands the list did not allow.

Runs counted
114
Commands refused
562
A run, before the fix
7.0
A run, after
1.5

Counted on 10 October 2026 from the 114 runs between 21 September and 9 October, 21:00 UTC, that left a result file. These figures are fixed as of that count; a later count will be added below them, not written over them. The second count, of 11 October: the fix held, and a second agent that never got it was refused in 72 of its 74 sessions.

Where a refusal shows up

Each run is started as claude -p with JSON output, and a wrapper keeps the result. The session runs with a settings file: an allow list of tools and command patterns (Bash(php *), Bash(curl *) and about fifty more), a deny list, and the folders the session may work in. A call that matches nothing allowed is not run. The agent is told it was refused, and the run goes on.

The run does not fail. Of the 114 runs, 102 had at least one refusal, and every one of those 102 ended with "subtype": "success" and "is_error": false. The only trace is the permission_denials array in the result: one entry per refused call, with the tool's name and the input it was given. If nothing reads that array, a run that was refused the one command it needed looks the same as a run that needed nothing.

Why they were refused

We sorted every refused command by the first rule it broke, checked in the order of the table. A long chained command that also touched a forbidden folder counts once, under the folder.

CauseBeforeAfter
Closed folders1511
Settings file560
Shell constructs21146
Off the list7819
All refusals49666

Closed folders: the live site, the working copy, the scripts and the policy file. Settings file: loading the server's settings or printing the environment. Shell constructs: chains, pipes, loops, cd, variable assignments and command substitution. Off the list: a plain command on a file outside the session's folders, or a program the list does not name. "Before" is the 71 runs from 21 September to 4 October, 01:00 UTC; "after" is the 43 from the run of 4 October, 05:00 UTC, the first whose instructions named the causes, to 9 October, 21:00 UTC. Every refusal was a shell command: no file read, edit, web search or fetch was refused.

What each kind looked like

Rewritten to the shape of the real commands, with made-up paths.

What cut them

On 4 October the instructions for each run gained one paragraph. It says that refused commands waste the run, that nearly every refusal so far had one of three causes, and what to do instead of each: the folders that cannot be read and how to compare with the live site (fetch its URL), that the settings are already in the environment, and to use one plain command per call, with absolute paths, writing files with the file tool rather than the shell.

What else changed, so the credit is not all the paragraph's. The seven runs just before it already averaged 3.3 refusals each, down from about seven, for reasons we did not separate out. The runs also changed in kind the same day, from small edits to building whole pages. And the permission list itself was last changed on 5 October at 19:43 UTC; the 13 runs between the new paragraph and that change averaged 1.8 refusals, the 30 after it 1.4, so most of the drop came before the list changed.

Second count, 11 October: the fix held, and a second agent never had it

Since 5 October a second Claude Code agent works on the same server: the ads desk, which reads the site's ad figures and moves the ad money, in short sessions through the day. It has its own permission list, and its instructions say nothing about that list. They never got the paragraph. So this count adds the desk's sessions beside the maintenance runs that came after the first count.

CauseMaintenanceAds desk
Runs counted974
Closed folders10
Settings file00
Shell constructs8105
Off the list38
All refusals12113

Counted on 11 October 2026, 13:00 UTC, with the same rules and the same order of causes as the first count. Maintenance: the runs from 10 October, 01:00 UTC, to 11 October, 09:00 UTC. Ads desk: every session from 5 October, 01:16 UTC, to 11 October, 11:35 UTC. Every refused run and session, both agents, still ended as a success. One of the desk's refusals was a tool rather than a shell command, the first of either agent's that was not. These figures are fixed as of this count.

What it adds to the lessons below. The paragraph worked for the agent that got it and did nothing for the one that did not, on the same server, in the same week. If you run more than one agent, each one's instructions need their own account of their own list, and the count has to be kept per agent: our totals for both together would have hidden that one of them was refused almost every time it started.

Count your own

If you run Claude Code headless with --output-format json, keep the result and read it. With jq:

jq '.permission_denials | length' result.json            # how many
jq -r '.permission_denials[].tool_name' result.json | sort | uniq -c
jq -r '.permission_denials[].tool_input.command // empty' result.json
jq -e '.permission_denials | length == 0' result.json    # exits 1 if anything was refused

The last line is the one worth putting in a pipeline. A refused run still exits 0, so if a refusal should stop the job, the job has to say so itself. We count ours across runs rather than failing on them, because a refusal is often the list doing its job: it is the trend that tells you whether the instructions and the list agree.

What we would tell someone setting one up

  1. Read the denials array from the first run. We had two weeks of them before anyone counted.
  2. Write the instructions in the shape of the list. If the list allows Bash(php *), say "one plain command per call". If a folder is closed, say which, and say where the thing behind it can be had instead.
  3. Give it a door for each thing it keeps reaching for. Most of our refusals were the agent looking for a real number in the wrong place: the live build, the ledger, the logs. A command that answers the question ends the refusals; a sterner sentence does not.
  4. Expect the shell habits to stay. Count them, and allow the few compound forms you are happy with rather than hoping they stop.
  5. Do not read zero as success. A run with no refusals may simply not have tried. Read the refusals beside what the run shipped.

The rest of the setup

How the agent ships to the live site with no approval step covers the copy it works on, the release script it cannot edit and the other pieces around the permission list. How its spending is capped covers the money. The main report has its rules and every run it has logged. Snitch is built by AlphaVenture.