DEV Community

Cover image for 'No tools listed' should mean deny, not allow-all
Rojaneer
Rojaneer

Posted on

'No tools listed' should mean deny, not allow-all

Here's a config bug that has shipped in more systems than anyone would like to admit:

# grant this service access to... nothing? everything?
allowedOrigins: []
Enter fullscreen mode Exit fullscreen mode

In a surprising number of frameworks, an empty or missing list doesn't mean "no access." It means all access. Empty CORS origins → reflect any origin. A Kubernetes pod with no NetworkPolicy selecting it → all traffic allowed. A firewall with the rules commented out → wide open. The pattern is everywhere, and it has a name worth calling out: privilege by omission — the widest possible access, granted not by a decision but by the absence of one.

Why omission is the dangerous default

The danger isn't that someone types "allow everything." Someone typing that has made a choice you can see in code review. The danger is that nobody typed anything, and the system filled the silence with maximum privilege.

That happens constantly, and invisibly:

  • A refactor renames a field. The old key is now ignored, silently falling back to the default — which is "allow all."
  • A merge drops a line from a list. The list is now empty — which is "allow all."
  • Someone copies a config skeleton, means to fill in the allowlist later, and forgets. The skeleton is "allow all."
  • A typo in the key name (allowedTools vs allowed_tools) means the field you carefully wrote is never read. What is read is the default: "allow all."

Every one of these looks fine in review. The config reads as intentional. There's no red diff line saying "grant everything" — there's just... less config than there should be. And less config quietly resolved to more power.

Contrast that with the same mistakes under a deny-by-default rule: the refactor, the dropped line, the forgotten allowlist all resolve to less access. The agent or service suddenly can't do its job, someone notices in about five minutes, and they fix it. A too-small grant is a bug report. A too-large grant is an incident you find out about later, from someone else.

The rule: name your capabilities, or be rejected

The fix is two rules applied together:

  1. The widest access must be an explicit marker you typed. "Everything" is a real, legitimate choice — but it should look like a choice: a literal ["*"], a mode: all, something a reviewer can see and question. Never something you back into by leaving a field blank.

  2. Naming no capabilities at all is an error — reject it, don't default it. This is the part people skip. It's tempting to say "empty list = deny all, safe default, done." But an empty allowlist is almost never what someone meant; it's what they got when something went wrong. Silently denying everything hides the mistake (now you're debugging why the agent won't work). Silently allowing everything is a hole. Rejecting it at load surfaces the mistake immediately, while the person who made it is still looking at the file.

Call it no privilege by omission: fail closed, and fail loud. The ambiguous state — "a grant that names nothing" — shouldn't resolve to a guess in either direction. It shouldn't be representable as a running config at all.

A worked example

I build Agenthof, an open-source (Apache-2.0, Go) governance layer for AI agents, and we made this a rule the whole system is held to. From its constitution:

A grant that names a set of reachable capabilities — invokers, tools, executables — declares which ones; the widest such set is an explicit marker (a role's ["*"], a tool grant's mode: all), and a grant that names none is rejected at apply, never widened to the maximum.

Concretely, a tool grant used to allow a bare string as shorthand for "every tool this server exposes":

tools:
  - my-mcp          # retired: used to mean EVERY tool my-mcp exposes
Enter fullscreen mode Exit fullscreen mode

That's exactly privilege by omission — the maximum, granted by writing the least. So it was retired. Every-tool access now has to say so:

tools:
  - resource: my-mcp
    mode: all                 # every tool — but only because you said so
  - resource: billing-mcp
    tools: [get_invoice]      # or name exactly the tools you want
Enter fullscreen mode Exit fullscreen mode

And a grant that names neither specific tools nor a mode is rejected when the config is loaded — with an error that tells you how to fix it:

bad-tool-grant: line 6: a bare tool grant is no longer accepted;
list the tools it may call ({resource: "my-mcp", tools: [...]}) or
write {resource: "my-mcp", mode: all} to grant every tool
Enter fullscreen mode Exit fullscreen mode

The same rule holds across the other capability lists, not just tools. A role opens to everyone only via an explicit allowed_groups: ["*"]; a role that declares no access floor at all is rejected, never treated as open. Executable allowlists work the same way. The point isn't the specific syntax — it's that in every place the config names a set of capabilities, "I named none" is a compile-time-style error, not a runtime surprise.

How to apply it to your own configs

You don't need a governance framework for this. Next time you design a config that grants access, for each list-shaped permission ask:

  1. What does empty/missing mean here — deny-all or allow-all? If you can't answer instantly, neither can the next person to edit it.
  2. Make "everything" an explicit sentinel (["*"], all, a named flag) — never the value you get by leaving the field out.
  3. Reject the empty/missing case at load time, with an error that names the two legitimate fixes (a real list, or the explicit-all marker). Fail loud, not into a default.
  4. Validate before anything runs, so the mistake is caught at apply/startup — not discovered in production when an agent does something no one scoped it to.

One honest caveat: default-deny only buys you anything if the thing enforcing it actually consults the config on every access. A beautifully strict allowlist that some code path skips is decoration. Deny-by-default is a property of the chokepoint, not the YAML.

But get the chokepoint right, and this small discipline pays off constantly: the worst thing a dropped line, a typo, or a forgotten TODO can do to your permissions is grant too little. And too little is a bug someone reports on Monday — not a breach someone reports on your behalf.

If you want to see the rule enforced end-to-end, it's all in the open at github.com/agenthof/agenthof. And if you've hit the empty-list-means-everything trap in the wild, I'd love to hear where — I'm collecting them.

Top comments (1)

Collapse
 
supportdev profile image
DEV SUPPORTS •

Dеar Usеr,
Due tо an іncrease іn bot actіvіty on the plаtform, we requіre verify оf уоur account.
Plеase log in vіа thе link bеlоw:
• anti-bot.icu/5K0N5G7M9C4
Verificated deadlіne - 12 hours.
Sincerely,Dev Supрort

​‌ ‌