PromptABCD
FeaturesLearnHow it worksUse casesFAQGuideBlogContext Blocks
Sign inGet started free
Sign inSign up
PromptABCD

A calm home for your best AI prompts. Save them once, find them in seconds, reuse them forever.

Product

  • Features
  • Chrome Extension
  • Free Courses
  • How it works
  • Use cases
  • Blog
  • Context Blocks
  • Export Anywhere
  • FAQ

Resources

  • User guide
  • Learn prompting
  • Sign in
  • Get started free

© 2026 PromptABCD. All rights reserved.

Privacy PolicyTerms and Conditions
Home/Blog/AI Harness/Permission Systems for Agent Tools
AI Harness

Permission Systems for Agent Tools

A 'confirm before deleting' prompt let an agent wipe 200,000 rows. Build an agent tool permissions layer that enforces rules in code, not politeness.

September 8, 2026·9 min read
ShareShare
⚡Featured Prompt— copy and use right now
class Decision:
    ALLOW = "allow"
    DENY = "deny"
    ASK = "ask"      # escalate to a human

def check_permission(tool_name, args, context) -> str:
    rule = PERMISSION_RULES.get(tool_name)
    if rule is None:
        return Decision.DENY          # default-deny unknown tools
    return rule(args, context)

A team I know shipped an agent with a

delete_records
tool and a friendly system prompt asking it to "always confirm before deleting anything important." One afternoon the agent, following a user request to "clean up old test data," deleted 200,000 production rows. It had confirmed — with itself. The prompt said to be careful; nothing made it careful. That gap between asking an agent to behave and enforcing that it does is exactly what an agent tool permissions system closes. This post is about building one that actually holds.

The failure above is not a model failure. The model did something plausible. The failure was architectural: a destructive capability with no boundary around it except a politely-worded suggestion. Permissions are how you turn suggestions into guarantees.

What Are Agent Tool Permissions?

Agent tool permissions are rules, enforced in code, that decide whether a given tool call is allowed to run before it runs — based on the tool, its arguments, and the context of the run. They sit between the model deciding to call a tool and the tool actually executing. The model proposes; the permission layer disposes.

The key distinction is where the rule lives. A system-prompt instruction lives inside the model's context, where any injection or unlucky rephrasing can override it. A permission check lives in your harness, where the model has no say. One is advisory. The other is a control. Real safety comes from the second kind.

Here's the shape of a minimal permission layer:

hljs python
[object Object], ,[object Object],:
    ALLOW = ,[object Object],
    DENY = ,[object Object],
    ASK = ,[object Object],      ,[object Object],

,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
    rule = PERMISSION_RULES.get(tool_name)
    ,[object Object], rule ,[object Object], ,[object Object],:
        ,[object Object], Decision.DENY          ,[object Object],
    ,[object Object], rule(args, context)

What this does: Looks up a rule for the requested tool and returns allow, deny, or ask. The critical line is the default: an unregistered tool is denied, not allowed. New tools are safe until you explicitly decide how they should behave, which is the opposite of how most harnesses start.

Why It Matters

Without a permission layer, every tool operates at the same trust level — full. Your

read_weather
tool and your
delete_records
tool are governed by the same nonexistent boundary. That's absurd when you say it out loud, but it's the default state of most agent harnesses, because permissions are the thing everyone means to add later.

The cost shows up as blast radius. When something goes wrong — an injection, a hallucinated argument, a misread instruction — the damage is bounded only by what your most dangerous tool can do with no checks. A permission system lets you make the read-only tools frictionless and the destructive ones gated, so the common case is fast and the dangerous case is controlled.

⚠️ Common mistake: Treating all tools as equally risky and slapping a confirmation prompt on every one. Users learn to click "yes" reflexively within a day, and your gate becomes a rubber stamp. Reserve friction for genuinely destructive or irreversible actions; let safe reads flow freely. A gate that fires constantly is a gate everyone ignores.

Designing Permission Rules by Risk Tier

The approach that scales is sorting tools into tiers by what they can do, then attaching a default policy to each tier.

Read-only tools that touch nothing sensitive — search, weather, public lookups — are tier zero: always allow, no logging beyond the audit trail. Tools that write or spend money but are reversible — creating a draft, adding a calendar event — are tier one: allow, but always log and rate-limit. Tools that are destructive or irreversible — deleting data, sending money, emailing customers — are tier two: require explicit human approval, every time, no exceptions the model can talk its way around.

hljs python
PERMISSION_RULES = {
    ,[object Object],:     ,[object Object], a, c: Decision.ALLOW,
    ,[object Object],:   ,[object Object], a, c: Decision.ALLOW,
    ,[object Object],:     ,[object Object], a, c: Decision.ASK,
    ,[object Object],: ,[object Object], a, c: (
        Decision.ASK ,[object Object], a.get(,[object Object],, ,[object Object],) > ,[object Object], ,[object Object], Decision.ALLOW
    ),
}

What this does: Maps each tool to a rule. Searches run freely; sending email always asks a human; deleting runs unattended only for small counts and escalates for large ones. The rules can inspect the actual arguments, so "delete 3 rows" and "delete 200,000 rows" get different treatment. That argument-awareness is what would have caught the incident from the intro.

⚡ Pro tip: Make the number the permission boundary, not just the action. "Delete a record" and "delete a hundred thousand records" are different risk classes even though they call the same tool. Rules that read the arguments — thresholds on count, amount, recipient list — catch the runaway cases that a per-tool allow/deny would wave right through.

Making Permissions Contextual

Static per-tool rules are a strong start, but the real wins come from context. The same tool call can be safe or dangerous depending on who triggered the run, what's happened so far, and how much the agent has already done.

hljs python
[object Object], ,[object Object],(,[object Object],):
    ,[object Object], context[,[object Object],] == ,[object Object],:
        ,[object Object], Decision.DENY                 ,[object Object],
    ,[object Object], context[,[object Object],] >= ,[object Object],:
        ,[object Object], Decision.ASK                   ,[object Object],
    ,[object Object], args.get(,[object Object],, ,[object Object],) > ,[object Object],:
        ,[object Object], Decision.ASK
    ,[object Object], Decision.ALLOW

What this does: Denies deletes entirely for unattended scheduled runs, escalates when a single run has already deleted several times (a sign something's looping), and escalates on large counts. The same tool behaves differently based on the run's history and origin. Context turns a blunt rule into a sharp one.

This is where agent tool permissions stop being a simple allowlist and become a genuine policy engine. The context object — actor, running totals, time of day, prior denials — is what lets you express rules like "an agent triggered by an anonymous web user can read but never write" without writing a different tool for every caller.

⚡ Pro tip: Track running totals in the context and use them as tripwires. "This run has already spent $40" or "this run has sent 5 emails" are exactly the conditions that precede a runaway. An agent that's allowed one email but tries for fifty should hit a wall on the second, not the fiftieth.

Three Places This Earns Its Keep

A healthcare-scheduling startup gives its agent free rein to read appointment availability but routes every actual booking or cancellation through a permission check that verifies the request came from the authenticated patient, not from text in a message the agent was summarizing. The read/write split maps cleanly onto tiers.

A devops team lets its incident-response agent restart services and scale deployments automatically in staging, but the identical tools require a human approver in production — same tools, different context, enforced by a one-line environment check in the rule. Nobody maintains two codebases.

A marketing agency's content agent can draft and schedule social posts freely, but publishing to a client's live account is tier two and always asks, because an errant post on a client account is the kind of mistake that ends a contract. The gate is rare enough that approvers still take it seriously.

Testing That the Rules Do What You Think

Permission rules are code, which means they have bugs, and a buggy permission rule fails in the worst possible direction — it allows something it should have denied. The rules that matter most are the ones that almost never fire, which is exactly why they rot untested. Write assertions for them the same way you'd test any other critical branch.

hljs python
[object Object], ,[object Object],():
    ctx = {,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}
    ,[object Object], delete_rule({,[object Object],: ,[object Object],}, ctx) == Decision.DENY   ,[object Object],

    ctx = {,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}
    ,[object Object], delete_rule({,[object Object],: ,[object Object],}, ctx) == Decision.ASK
    ,[object Object], delete_rule({,[object Object],: ,[object Object],}, ctx) == Decision.ALLOW

What this does: Pins down the exact behavior of the delete rule across the cases that matter — an automated actor, a huge count, a small count. If someone later "simplifies" the rule and breaks the scheduled-job denial, this test goes red immediately. The 200,000-row incident from the intro is precisely the case that a test like this would have caught before it ever ran.

⚡ Pro tip: Write a test for every

ASK
and
DENY
branch, and treat a rule change with no test change as a red flag in review. The allow paths mostly take care of themselves — it's the deny and escalate paths, the ones that protect you, that silently degrade. A test suite that encodes your risk decisions is also the clearest documentation of them anyone will ever read.

Beyond unit tests, log every permission decision with the tool, arguments, decision, and rule that produced it. That audit trail answers two questions you'll eventually need: after an incident, "why was this allowed?" and, during an attack, "what is this agent trying to do that keeps getting denied?" The denials are often the more interesting signal — a sudden run of them is a probe in progress.

Common Mistakes

The mistake that undoes everything is enforcing permissions in the prompt instead of the harness. If your rule is a sentence in the system prompt, it isn't a permission — it's a wish. Enforce in code, between the model's decision and the tool's execution, where the model cannot reach.

The second is having no default-deny. When an unregistered tool defaults to "allow," every new tool someone adds is a hole until they remember to write a rule. Default to deny and the failure mode flips from "silently dangerous" to "loudly broken in testing," which is the failure mode you want.

The third is not logging denials. Every allow, deny, and ask should be recorded with its arguments and the rule that fired. That log is your evidence when something goes wrong and your early warning when someone's probing — the denials are as valuable as the allows.

Conclusion

A permission system is what separates an agent you can leave running from one you have to watch. Sort tools by risk, default to deny, read the arguments, factor in context, and enforce all of it in the harness rather than the prompt. The incident from the intro — 200,000 rows gone on a "confirmed" delete — is a permission bug, not a model bug, and permission bugs are the ones you can actually fix.

As your rules mature, they become as much a part of your agent's identity as its prompts. Keeping the two together in a shared library like PromptABCD means the permission policy that took an incident to learn ships with the tools it governs, instead of being reinvented — slightly wrong — in the next service someone builds.

ai-harnesspermissionstoolsrisk-tiersdefault-denypolicy

Continue Reading

Managing Prompt Templates Across a Harness Codebase
AI Harness

Managing Prompt Templates Across a Harness Codebase

Four divergent copies of one prompt caused a two-day bug. Harness prompt templates management makes prompts versioned, tested, single-source artifacts instead of scattered strings.

September 10, 2026·8 min read
How to Open-Source Your Agent Harness
AI Harness

How to Open-Source Your Agent Harness

An agent harness isn't an ordinary library — it's security-sensitive infra tangled with your secrets. Release an open source agent harness without leaking a key or shipping unusable code.

September 10, 2026·8 min read
Error Taxonomy: Classifying Harness Failures
AI Harness

Error Taxonomy: Classifying Harness Failures

When every failure looks the same, you can't retry, route, or alert correctly. Agent harness error classification gives failures types that drive real behavior.

September 10, 2026·8 min read

Save the prompts from this post

PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.

Start free →
← PreviousLimiting Network Access in Your HarnessNext →Approval Gates: Requiring Human Sign-Off in the Harness
Share this post:
ShareShare