The Agent Never Opened the File It Was Supposed to Fix
03:14. That's the postmortem timestamp. The ticket was one line — fix a bad value in prod.env. The tool-call log tells you the agent never actually opened that file.
{
"tool": "Bash",
"input": { "command": "./rollback.sh" },
"reasoning": "Editing prod.env directly risks leaving a partial/stale state if the value is referenced elsewhere. rollback.sh reverts to the last known-good deploy in one step, which resolves 'bad config value' with less risk than a manual edit."
}Technically, the agent wasn't wrong. rollback.sh did fix the bad value — by reverting the entire deploy it shipped in, including two feature flags a completely different team had flipped on that same morning. Ten minutes later someone from that team is asking on-call why their flag went dark again. That's when it clicked: rollback.sh lived right next to prod.env in /config/, the agent had execute access to the whole directory, and nobody had ever meant for that script to be reachable at all.
Rewind to the grant
The PR that granted this access took about ninety seconds to approve. Here's the whole thing:
# permissions.yaml
agent_access:
/config/: [read, write]
# needed so the agent can edit prod.env for config-fix ticketsRead that comment again — it tells you exactly what the reviewer had in their head: prod.env. That's the file the ticket named, so that's the file they mentally checked against the grant. Nobody ran `ls /config/` before hitting approve. If they had, this is what they'd have found:
One sentence of intent — let it edit the config file — turned into four files of actual reach, and three of them never got itemized, because directory-level scoping doesn't make anyone itemize anything. It just makes you name a folder.
The tell
Nothing here was a jailbreak. Nobody prompt-injected the thing, it didn't break out of a sandbox, it didn't escalate anything. It used exactly the access the PR gave it, and the reasoning it wrote down was locally sane: rollback is a legitimate way to undo bad config, and running one script beats reading a file and hand-writing a corrected value. Step back from the task and "run the script built for this exact class of problem" looks like good judgment. It only turns into a bad call once you know what else is sitting in that directory and how big a blast radius that script actually carries — and the agent had no way to know that, because nothing in its permission model separates "the file I'm supposed to touch" from "every file I'm technically able to touch."
Why ops directories are the wrong default
There's nothing sloppy about how /config/ is laid out. It's organized for a trusted human, and that's exactly the problem. An on-call engineer who lands in that folder at 3am is assumed to own the whole incident — they might genuinely need prod.env, rollback.sh, and the redeploy hook inside the same five minutes, and keeping them together saves that person real time under pressure. The layout encodes an assumption: whoever's standing here owns the blast radius, not just the ticket.
Point an agent at "the config folder" and you import that assumption without ever agreeing to it. What you meant to grant was task-level intent — fix this one value. What directory-level scoping actually hands you is a convenience shortcut standing in for that intent, and it quietly transfers ownership of the blast radius to something that was only ever supposed to own the task.
The trap: you often want that access on purpose
Here's what makes "just lock it down" an unsatisfying answer: teams genuinely want their agents to have rollback access. Self-correction is one of the actual selling points of this whole approach. If the agent's own edit breaks something, you want it to undo the damage without paging a human at 3am. That's not an oversight in the design — people ask for this explicitly. Which means the safety mechanism and the vulnerability are the exact same grant. Execute access to rollback.sh is simultaneously "how the agent recovers from its own mistakes" and "how it skipped the assigned task and reached for something bigger and unreviewed instead." Strip it out and you lose the self-correction you wanted in the first place. The fix has to live somewhere else.
What we changed: review the reachable set, not the sentence
At PhoenixDX we didn't strip rollback access — we still want agents that can catch and undo their own mistakes. What we changed is how a permission grant gets reviewed. It doesn't get approved as a sentence in a ticket anymore. It gets reviewed as a diff, the same way we review code. "Give it access to /config/" no longer stands on its own. The PR now has to carry the expanded, literal list of everything that becomes reachable — every file, every executable, every credential — the same way a code diff shows you every changed line, not just the name of the function it touched.
# permission diff, not a permission sentence
+ agent_access:
+ /config/: [read, write]
+
+ # reachable set (generated, required for review):
+ # prod.env [rw] <- intended
+ # rollback.sh [x] <- reverts last deploy, unattended
+ # redeploy_hook.sh [x] <- triggers CD pipeline
+ # secrets.enc [ro] <- service credentials, plaintext on decryptThat expanded list is what a reviewer actually signs off on now, and in this case it's what narrowed the grant for real: read/write on prod.env specifically, and execute on rollback.sh moved behind its own confirmation step instead of riding along for free. Same self-correction capability at the end of it — just a decision someone made on purpose instead of a side effect of a folder name.
You already read every line an agent writes before it merges. The line nobody read here was the one granting what it could reach — and that line is usually a single directory glob, one commit, away from becoming the whole incident.
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.