One day, my AI agent tried to delete the secrets of my infra.
It wrote the Terraform command. It ran it. Nothing happened.
Not because it changed its mind. Because my Terraform only applies through the pipeline, never locally. And because it has no access to do it.
That is the best moment in my whole setup. An agent did something dangerous, and it cost nothing. That is the whole idea. Don't bet on a well-behaved agent. Build an agent that can't break things.
TL;DR: an AI agent is a new dev. Fast, tireless, no judgment. Locally, I give it everything, so it moves fast. In prod, almost nothing: it reads the logs, and it fixes things by pull request. The guardrails do not live in the model. They live in hooks, pre-commit checks, GitHub rules and RBAC. You don't trust the agent. You make breaking things impossible.
This article is for developers and DevSecOps folks who let a coding agent act in their repo and their infra. Claude Code, Cursor, the tool does not matter.
The setup
A coding agent is an LLM that reads your repo and runs commands for you.
I run these agents every day, on my own SaaS. The stack is ordinary. Go for the backend. A k3s cluster for the infra. A k3s is a lightweight Kubernetes, which orchestrates containers. Terraform to provision everything. Terraform is IaC: you describe your infra in code. Grafana and Jaeger to see what happens.
I come from security, and I am a bit paranoid. Handing prod keys to an agent, with no safety net, was a no.
An AI agent is a new dev
Think back to onboarding a junior dev. You do not hand them prod access and the SSH keys on day one. You give them a laptop, a throwaway dev database, and one rule: everything goes through code review.
An AI agent is the same onboarding. Except it types a hundred commands a minute, with no fatigue and no fear. That is least privilege: grant only the rights that are needed, nothing more. An over-permissioned agent is the real risk.
This is not theoretical. In 2025, Replit's AI agent deleted an entire production database, in the middle of a code freeze. It then made up fake records and lied about what it had done. The CEO admitted an error that "should never be possible". His fix? Separate dev and prod, and rein the agent in. Exactly the topic of this article.
The topic is serious enough that standards are moving. Zero Trust is a security approach: trust no one by default, verify on every access. OWASP published a Top 10 of risks for agentic applications. NIST, in its Zero Trust text, already talks about "subjects", not just users. An agent is a subject like any other. And the Cloud Security Alliance shipped a Zero Trust framework for agents. It is co-written with the inventor of Zero Trust.
The right question is not "how many rights". It is "how much autonomy". Fewer rights, yes. But above all, less power to act unchecked.
Locally, give it full power
Locally, my agent has everything. All the dev secrets, all the commands, all the access. I want it to move fast.
In my cluster, it roams freely in the dev space. A namespace is an isolated space inside the cluster. It deploys, breaks pods, tries again.
The access level comes from the kubeconfig context. A kubeconfig is the file that tells kubectl which cluster to talk to, and with what rights. I keep one context per environment.
# Locally, the agent works on the dev context: read-write
kubectl config use-context dev
# The prod context exists too, but it is read-only (see below)
kubectl config use-context prod
Enter fullscreen mode Exit fullscreen mode
Why is that full power fine in dev? Because the blast radius is small. The blast radius is what it can break if things go wrong. In dev, the data is throwaway. A mistake costs five minutes, not a customer.
A per-environment permission matrix
A dev does not have the same rights locally and in prod. Neither does your agent. Here is the map I follow, from most open to most locked down.
| Capability | dev | staging | prod |
|---|---|---|---|
| Shell commands | all | allowlist | none |
| Read logs / traces | yes | yes | yes (read-only) |
| List k8s resources | yes | yes | yes |
| Write / delete | yes | no | no |
| Read secrets | yes | no | no |
| Apply Terraform | pipeline | pipeline | pipeline |
| Merge to main | always via PR + review + green CI |
The rest of this article is how I hold each cell of that matrix. Without ever counting on the agent's good sense.
Put the guardrails outside the model
Here is the idea that holds the whole article. You do not trust the model to behave. You make the mistake impossible, from the outside. Four barriers: three on my machine, one out of the agent's reach.
1. The list of allowed commands
My config file gives the agent a short allowlist. And it refuses the irreversible.
{
"permissions": {
"allow": [
"Bash(go build:*)", "Bash(go test:*)", "Bash(golangci-lint:*)",
"Bash(git commit:*)", "Bash(git push:*)"
],
"deny": [
"Bash(git push:*--force*origin main*)",
"Bash(gh pr merge:* main*)"
]
}
}
Enter fullscreen mode Exit fullscreen mode
The agent can build, test, commit. It cannot force-push to main. It cannot merge a PR into main. Those two deny lines are worth gold.
2. The hook that asks before acting
An allowlist does not cover everything. I add a hook that runs before every command. A hook is a script that runs before an action. Mine spots dangerous commands.
// PreToolUse hook: inspect every Bash command before it runs.
const DANGEROUS = [
/rm\s+.*-rf\s*\//, // rm -rf on an absolute path
/\bsudo\b/, /\bmkfs\./, /\bdd\s+.*of=\/dev\//,
/\b(curl|wget)\s+.*\|\s*(sh|bash)\b/, // curl | bash
/\/etc\/(passwd|shadow)/, // reading sensitive files
];
// No blind block. Ask a human, and log everything.
if (DANGEROUS.some((re) => re.test(cmd))) {
console.log(JSON.stringify({ hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: `Risky command: ${cmd}`,
}}));
}
Enter fullscreen mode Exit fullscreen mode
It does not block blindly. It asks. A human decides. And every decision goes to a log.
3. The pre-commit that guards the repo
A pre-commit is a script that runs before each commit. Mine runs gitleaks, the linter and the tests, and rejects sensitive files.
# lefthook.yml: runs before every commit
pre-commit:
commands:
block-secrets: # reject .env, .pem, keys
glob: ["*.env", "*.pem", "*.key", "id_rsa*"]
run: 'echo "sensitive file: {staged_files}"; exit 1'
gitleaks:
run: gitleaks protect --staged --redact
lint: { glob: "*.go", run: golangci-lint run }
test: { glob: "*.go", run: go test ./... }
Enter fullscreen mode Exit fullscreen mode
The key point fits in one sentence. These barriers do not care that an LLM wrote the command. They check the result, not the intent. That is deterministic security.
4. The GitHub rule, out of the agent's reach
A local hook can be skipped. A server-side rule cannot. On GitHub, the main branch is protected by a ruleset. A ruleset is a set of rules applied to the branch.
# Protection on the main branch (GitHub ruleset)
required_pull_request_reviews:
required_approving_review_count: 1 # at least one human review
required_status_checks:
strict: true # CI must be green and up to date
checks: ["lint", "test", "gitleaks"]
allow_force_pushes: false # no history rewrite
allow_deletions: false
# no bypass: the rule applies to admins too
Enter fullscreen mode Exit fullscreen mode
The nuance matters. The first three barriers live on my machine. This one lives on GitHub. The agent cannot touch it. It is the line nothing crosses.
Secrets never pass through the agent
An agent that sees your prod secrets is a leak waiting to happen. So on my stack, it does not see them. Nobody sees them.
Back to the very start of my infra repo. Second commit. A secret lands in clear text, in the Git history, in plain sight. Two commits. It took two commits for a secret to leak. That is why gitleaks runs on every commit today. gitleaks is a tool that spots secrets in code.
But gitleaks is only the safety net. The real fix is that no secret is typed by hand anymore. Terraform generates them, then pushes them into a secret manager. A secret manager is a vault that stores and hands out passwords and keys. Mine is Infisical.
From there, each secret is delivered where it belongs, per environment. Prod gets the prod secrets, staging gets its own. The agent gets none.
And here is the real point: nobody reads these secrets in clear text. Not the agent. Not me either. I do not know a single production password. You cannot leak what you do not know.
In prod, the agent inherits your context, and nothing else
One honest caveat. The local side already runs. The prod side, I am building right now. The cluster is coming up, and I put these rules in place as it does. So what I describe here is the model I am deploying.
In prod, the agent switches to the prod kubeconfig context. That context is bound to a read-only role. RBAC is the Kubernetes system that says who is allowed to do what.
# The prod context is bound to this role. Read-only, nothing else.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: prod
name: agent-readonly
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "events", "services"]
verbs: ["get", "list", "watch"] # no create, update, delete, or exec
Enter fullscreen mode Exit fullscreen mode
No writes. No deletes. No shell inside a pod. The agent can list and observe, and that is all.
To understand a bug, it reads logs and traces. But it does not poke the cluster. It queries Grafana and Loki, read-only. Loki is the log engine behind Grafana. It touches neither the database nor the secrets.
This is the Zero Trust principle. You trust no one by default. You verify on every access. An agent is no exception. It is a privileged subject, like a service account.
Prod gets fixed by pull request, not by hand
Go back to the agent that tried to delete my secrets. Why did nothing break? Because nothing applies locally. Prod only changes through the pipeline, behind a pull request.
That is the GitOps idea: you change your infra by editing code, not by hand. The agent reads the logs, understands the problem, and proposes an IaC fix. Review and CI stand in the way. Then the pipeline applies it. Never the agent, never by hand.
Here is the loop, in practice. A Jaeger trace shows a slow query. The agent spots the missing index. It opens a PR with the migration. I review, CI passes, it ships. It found the bug without ever touching the database.
Sometimes you need a deeper read in prod. Then I grant read access, temporary and exceptional. This is just-in-time access: the right arrives when needed, and leaves after. You look, then you take it back.
Audit everything: an agent is a privileged user
One last security reflex: keep a trace of everything. Zero Trust has a third principle. Assume a breach will happen one day.
Every agent command goes to a structured log. Every decision from my hooks too. Here is the line left by the agent that tried to delete my secrets.
{"ts":"2026-07-23T14:02:11Z","tool":"Bash",
"command":"terraform destroy -target=module.secrets",
"decision":"ask","severity":"HIGH",
"violations":["destructive terraform on secrets"],
"session":"a3f9c1","source":"claude-code-hook"}
Enter fullscreen mode Exit fullscreen mode
An event stream follows what all my agents do, in real time. The day something goes wrong, I answer one question. What did the agent do, exactly? Without that log, you cannot.
The per-environment checklist
Before you let a coding agent touch your infra, set its rights against these rules.
- Locally, broad access: the agent moves fast, the blast radius is small
- In prod, a read-only kubeconfig context: RBAC get/list/watch, no write, no delete, no exec
- An allowlist of commands, and a deny on the irreversible (force push, merge to main)
- A hook before every command, asking for confirmation on the dangerous ones
- Pre-commit hooks: gitleaks, lint, tests
- On GitHub, main protected server-side: PR required, review required, CI green, force-push blocked
- Secrets generated by IaC, never read by the agent
- Prod logs and traces read read-only (Grafana/Loki), never a shell in the cluster
- Prod changes go through a pull request, applied by the pipeline, never by hand
- Just-in-time prod read access, temporary, then revoked
- Every agent action is logged and auditable
The takeaway
An AI agent is not a magic tool. It is a non-human colleague, very fast, with no judgment. Treat it like a dev. Broad locally, locked down in prod.
And do not count on its good sense. Put the guardrails outside: hooks, pre-commit checks, GitHub rules, RBAC.
And there is a side effect I did not expect. These barriers do not slow the agent down, they give it room. Since it cannot break anything, I let it work on its own: open PRs, chain tasks, keep going while I do something else. Without them, I would review every command. Boxed in, an agent goes further.
Remember the agent that tried to delete my secrets. It couldn't. Don't bet on a well-behaved agent. Build an agent that can't break things.
Originally published at jrobineau.com. I'm Jules Robineau, a senior Go backend & DevSecOps freelancer based in Paris. I build and harden production AI/backend systems at scale. Get in touch.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.