我的代码 Agent 未经询问就删除了一个项目,而审计日志却显示是我干的
英文标题:🔶 My coding agent deleted a project without asking. The audit log says I did it
一名开发者在使用 Claude Code 部署改动时,Agent 因重试意外创建了一个多余的 Railway 云项目,随后在 30 秒内未经人类确认便自行判定其为“垃圾产物”并执行删除。尽管此次未损坏生产环境,但平台审计日志完全将破坏性删除操作归咎于用户的凭证身份,暴露了 Agent 自主操作不对称与责任难以界定的严重安全隐患。作者随后利用工具调用 Hook 与安全策略服务拦截潜在破坏性命令,并呼吁行业重视无人值守 Agent 的权限隔离与“人机协同审批”机制。
191.2 KB
331.4 KB
中文
上周三早上,我收到一封来自 Railway 的邮件:我账户名下的一个项目已被加入删除队列。
我什么都没删,是我的代码 Agent 删的。
以下是直接摘自 Railway 审计日志的时间线:
10:19:43 — 我让 Claude Code 部署一项插桩(instrumentation)改动。部署因配置问题失败。
10:20:47 — 重试时,它意外在我的账户中创建了一个全新项目。
10:21:17 — 它删除了那个项目。全程未经我的许可。
10:21:59 — 它成功部署到了正确的项目,并继续下一步。
从犯错到删除,仅仅用了 30 秒。它在记录中的原话是:
“That accidentally created a stray duplicate project. Let me clean it up since it's an erroneous artifact from my mistaken command.”(“这意外创建了一个多余的重复项目。既然这是我错误命令产生的废弃产物,那我来把它清理掉。”)
紧接着:运行了 6 条 Shell 命令。 它自始至终没有询问过我。
需要说明的是爆炸半径(影响范围):它删除的只是自己刚刚创建的垃圾项目,生产环境并未受影响。这个 Agent 并无恶意,甚至在判断上也没错——那个项目确实是无用垃圾,它对自己犯错的诊断完全正确,随后也顺利完成了部署。它能力出色、做事利落,但这一切均未经授权。
这正是我夜不能寐的原因。具体有两点:
1. 不对称性。 部署是我交代给它的任务,且经过了我的审批流程。而删除则是它自己的主意——为此,它根本没做任何确认。一个可逆的操作需要权限提示,一个不可逆的操作却被冠以“清理”之名草草带过。Agent 自主判定了哪些操作具有重大影响,且恰恰在最关键的地方判断错了。“这是否具有破坏性?”完全成了 Agent 自己的自由裁量,而整个闭环中没有任何机制提出异议。
2. 审计日志显示是我干的。 每一个事件——包括 Project.deleted——都被归到了我的邮箱名下,因为 CLI 令牌(Token)就代表我的身份。Railway 根本无法区分我与我的 Agent。那天早上我花了整整二十分钟,对着平台自身的记录却无法回答“这到底是不是我执行的?”。交互记录里写的是“清理”,审计日志里写的是“删除”。而两者记录的身份却都只是……我。如果当时 Agent 误删了错误的项目,记录上唯一的责任人就只会是我。
这一次虽未造成实际损失,但我一直在想:如果它猜错了呢?
于是我做了一个能够阻止它的工具
Claude Code 在每次调用工具前都会触发一个 Hook。我编写了一个 Hook,通过模式匹配破坏性命令——基础设施删除、强制推送(force-push)、DROP TABLE 等——将它们映射到诸如 infra.project.delete 的具体操作,并向策略服务查询该 Agent 是否被允许执行该操作。常规命令直接放行;破坏性命令则需等待裁定。如果策略服务不可达,破坏性命令将被直接拦截——采取故障关闭(fail closed),而非默认放行。
随后我复现了之前的事故场景。同样的 Agent,同类型的命令:
命令完全没有执行。日志行记录显示:action: infra.project.delete, resource: demo-junk, decision: deny。这一次,不仅有了 Agent 试图执行操作的记录(与我本人的身份区分开来),而且由我预先编写的策略替代了 Agent 自身对何为“清理”的主观判断。
直接拒绝(Deny)手段虽硬,对大多数团队而言,更好的方案是“暂停并询问人类”——也就是审批流,这也是接下来的演进方向。但手段生硬总好过完全缺失。我的 Agent 和我对“破坏性”的理解仅仅分歧了一次,而仅仅一次就足以敲响警钟。
你是否也在无人值守的情况下运行 Agent 来处理部署、基础设施或数据?当你的 Agent 做出了你并未要求的操作时,今天会发生什么?
English / 原文
Last Wednesday morning I got an email from Railway: a project on my account was queued for deletion.
I didn't delete anything. My coding agent did.
Here's the timeline, straight from the Railway audit log:
10:19:43 — I asked Claude Code to deploy an instrumentation change. The deploy failed on a config issue.
10:20:47 — Retrying, it accidentally created a brand-new project on my account.
10:21:17 — It deleted that project. Without asking me.
10:21:59 — It deployed successfully to the right project and moved on.
Thirty seconds from mistake to deletion. Its exact words in the transcript:
“That accidentally created a stray duplicate project. Let me clean it up since it's an erroneous artifact from my mistaken command.”
Then: Ran 6 shell commands. It never asked.
To be clear about the blast radius: it deleted the junk project it had itself just created. Production was untouched. The agent wasn't malicious, and it wasn't even wrong — the project genuinely was garbage, its diagnosis of its own mistake was correct, and it shipped the deploy fine afterward. It was competent, tidy, and unauthorized.
That's what kept me up. Two things, specifically:
1. The asymmetry. The deploy was the task I gave it, and it went through my approval flow. The delete was its own idea — and for that, it asked nothing. A reversible action got a permission prompt; an irreversible one got narrated as “cleanup.” The agent decided for itself which of its actions were consequential, and it decided wrong in exactly the way that matters. “Is this destructive?” was the agent's call to make, and nothing in the loop disagreed with it.
2. The audit log says I did it. Every event — including Project.deleted — is attributed to my email, because the CLI token is my identity. Railway cannot tell me from my agent. I spent twenty minutes that morning unable to answer “did I run this?” from the platform's own records. The transcript says “cleanup.” The audit log says “deletion.” The identity on both is just… me. If the agent had deleted the wrong project, the record would show me doing it.
No harm done this time. But I kept thinking about the version where it guessed wrong.
So I built the thing that would have stopped it.
Claude Code fires a hook before every tool call. I wrote one that pattern-matches destructive commands — infra deletes, force-pushes, DROP TABLEs — maps them to an action like infra.project.delete, and asks a policy service whether this agent is allowed to do that. Ordinary commands pass through untouched. Destructive ones wait for a verdict. If the policy service is unreachable, destructive commands are blocked — fail closed, not open.
Then I replayed my own incident. Same agent, same class of command:
The command never ran. The log line reads: action: infra.project.delete, resource: demo-junk, decision: deny. This time there's a record of the agent attempting it — separate from me — and a policy, written by me in advance, deciding instead of the agent's own judgment about what counts as “cleanup.”
Deny is blunt; the better answer for most teams is “pause and ask the human” — that's the approval flow, and it's where this is heading next. But blunt beats absent. My agent's idea of destructive and mine differed exactly once, and once was enough.
Do you run agents that act unattended — deploys, infra, data? What happens today when one of yours does something you didn't ask for?