There are two ways to make a coding agent safer.
One is to ask the model to be careful. Add instructions to the system prompt: “do not delete important files,” “ask before running destructive commands,” “never access credentials.” This is what most agent frameworks do today. It is easy to implement and it sort of works.
The other is to enforce policy at the boundary, outside the model, on the actual tool calls. This is what agentjail does.
We think the second approach is fundamentally better. Here is why.
The model is not the right enforcement point
A system prompt is a suggestion. A well-crafted one, backed by RLHF and careful training, but a suggestion nonetheless. The model can be jailbroken. It can hallucinate compliance while violating the spirit of the instruction. It can interpret “do not delete important files” differently than you would. It can decide that a file is not important.
More subtly, model-based safety degrades under load. Long conversations accumulate context that pushes safety instructions further from the attention window. Complex multi-step tasks create situations the system prompt never anticipated. And every model update can silently change how instructions are interpreted.
None of this is the model’s fault. It is the wrong tool for the job. Access control should not be probabilistic.
What deterministic policy looks like
An agentjail rule is a Rego expression that evaluates a tool call and returns a verdict: allow, deny, or ask. The evaluation is deterministic. The same input produces the same output every time, regardless of conversation length, model version, or phase of the moon.
deny[msg] {
input.tool == "Bash"
input.command_binaries[_] == "curl"
not startswith(input.tool_input.command, "curl -fsSL https://")
msg := "Blocked: curl command does not use safe flags"
}
This rule fires or it does not. There is no “usually” or “depending on context.” There is no temperature parameter that affects whether your credentials get exposed. The rule is a few lines of plain text that anyone on the team can read, review, and version control.
Five things policy gives you that prompts cannot
1. Auditability. When agentjail blocks a call, the log shows exactly which rule fired, what it matched, and why. Try auditing a system prompt rejection. The model said no. Why? “Based on my guidelines.” Which guideline? Silence.
{"ts":"2026-06-16T14:23:01Z","verdict":"deny","rule":"library/no-curl-unsafe-flags","tool":"Bash","command":"curl http://169.254.169.254/latest/meta-data/iam/","matched_binary":"curl","session":"abc123"}
2. Testability. agentjail rules have unit tests. You write a tool
call input, assert the expected verdict, run the suite. A rule that
should block rm -rf / but does not is a failing test, caught before
deployment. How do you unit test a system prompt? You do not. You run the
model and hope.
test_blocks_imds_curl {
result := deny with input as {
"tool": "Bash",
"tool_input": {"command": "curl http://169.254.169.254/latest/meta-data/"},
"command_binaries": ["curl"]
}
count(result) > 0
}
test_allows_registry_curl {
result := deny with input as {
"tool": "Bash",
"tool_input": {"command": "curl -fsSL https://registry.npmjs.org/"},
"command_binaries": ["curl"]
}
count(result) == 0
}
Run agentjail policy test. A failing test means a broken rule — caught before any agent runs it.
3. Composability. Rules stack. A team deploying to AWS might layer library/no-aws-destructive on top of the defaults, disable no-curl-unsafe-flags because their agents legitimately run curl pipelines, and add a custom rule that blocks force-pushes outside business hours. That configuration is three lines in a policy file. A system prompt is a monolith. Change one sentence and the interaction effects are unpredictable.
4. Speed. OPA evaluates Rego rules in microseconds. agentjail’s median is ~8 ms on warm evaluations for the full decision pipeline (parse, evaluate, log, respond). An LLM-based safety check adds seconds of latency per tool call. Multiply by the hundreds of tool calls in a typical agent session and you have added minutes of wall-clock time to every task.
5. Consistency across models. The same agentjail policy works identically whether the underlying agent is Claude, GPT, Gemini, or a local model. You write the rules once. Switch models and nothing changes. System prompts need to be re-tuned per model, per version, sometimes per task.
“But policy is rigid”
The most common objection: deterministic rules cannot handle nuance. The
model “understands” that rm -rf node_modules is safe but rm -rf / is
not. A regex rule blocks both or neither.
This is true of bad rules. It is not true of policy in general.
agentjail’s structured command parsing (added in v0.2.0) gives rules
access to parsed binary names and command structure. A rule can allow
rm when the target is inside the project directory and deny it
otherwise. It can allow curl to known package registries and block it
everywhere else. It can allow git push to the current branch and deny
force-pushes to main.
deny[msg] {
input.tool == "Bash"
input.command_binaries[_] == "git"
contains(input.tool_input.command, "push")
contains(input.tool_input.command, "--force")
msg := "Blocked: force push requires explicit approval"
}
The expressiveness ceiling of Rego is high. You can match on file paths, environment variables, command structure, network targets, and combinations of all of them. The rules we ship cover the obvious cases. The rules you write cover your specific cases.
And for the genuinely ambiguous calls — the ones that depend on intent, not structure — agentjail has the “ask” verdict. The call is paused, the user is prompted, and their decision is logged. This is not the model guessing whether you would approve. It is you actually approving.
The layering argument
We are not saying system prompts are useless. They are a good first layer. An agent that has been instructed to be careful will attempt fewer dangerous things, which means fewer tool calls for agentjail to evaluate, which means fewer interruptions for the user.
But the system prompt is the seatbelt. agentjail is the guardrail. You want both. You do not want to rely on the seatbelt to keep the car on the road.
What this costs
Deterministic policy has real costs:
Rules need maintenance. New tools, new agents, new attack surfaces mean new rules. Someone has to write them. We ship default packs and will keep expanding them, but your team’s specific constraints are yours to encode.
False positives are your problem. A model can use context to
distinguish a safe rm from a dangerous one. A rule matches or it does
not. The structured parsing in v0.2.0 fixed the class of false positives caused by matching binary names in file paths, but they still happen. When they do, you adjust the rule.
There is a learning curve. Rego is not hard, but it is another thing to learn. We have tried to make the defaults good enough that most users never write a rule, but power users will need to learn the syntax.
We think these costs are worth paying. A model that “usually” blocks credential access is not safe. It is lucky.
The alternative is trusting a probabilistic system with deterministic consequences. For personal projects, that might be fine. For anything that touches production systems, credentials, or other people’s code, “usually” is not a safety posture. It is an optimistic default.
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
agentjail DENY library/no-curl-unsafe-flags
rule: curl to non-https or without safe flags
cmd: curl http://169.254.169.254/...
took: 2.1ms
Tool call blocked. The agent was notified.
curl -fsSL https://agentjail.io/install.sh | sh