Giving Least Privilege to an AI Bot That Could Run Anything: Removing bypassPermissions

Flipping on "bypass all permissions" for an AI agent is a common convenience — it just works. But that bot could run anything on the server: delete files, kill services, read secret files, even place real trades. This is the story of narrowing an always-on AI bot from "allow everything" to a whitelist-based least privilege, and the bypass traps I hit along the way.

1. Starting point: the danger of "bypass all"

When you first stand up an AI bot driven over a messenger, permission prompts feel annoying, so it's tempting to run it in "don't ask, just do it" mode. The problem: if the bot errs (hallucinates) or is hit by prompt injection, that unlimited authority becomes the attack surface. Concretely, this bot could:

  • delete files with rm
  • kill or restart services
  • read secret files (.env, etc.)
  • place real buy/sell orders via finance tools

2. Evidence first: 30 days of real usage logs

Cut permissions blindly and the bot becomes useless. So first I looked at data: "what does this bot actually use?" Aggregating the last 30 days of session records:

  • mostly read-only commands — viewing logs, checking status, reading files, searching
  • file edits/writes occasionally
  • trade orders, shell execution, and secret-file writes: zero over 30 days

The conclusion was clear. What the bot actually needs is "read + light editing," and the dangerous stuff it never used anyway. Blocking what it doesn't use costs nothing.

3. Design: allow / deny whitelist + a headless-safe mode

I turned off "bypass all" and switched to explicit allow/deny lists.

# Before
--permission-mode bypass  # run anything

# After
--permission-mode dont-ask          # deny anything not allowed, no prompt
--settings whitelist.json           # controlled by allow/deny lists

Three core principles:

  1. allow only the observed read-only set: status checks, logs, file read/edit, search — the things it actually used.
  2. deny wins: even where it overlaps allow, deny takes precedence. Destructive commands (rm, service deletion, force push, permission changes), trade-execution tools, and secret-file read/write are explicitly denied.
  3. a denial must not stall: in headless mode a permission prompt waits for input and freezes. So use a "deny without asking" mode. The bot reports "I couldn't do that — no permission" and moves on.

4. Verify: does it actually get denied?

Don't trust config — measure it. I fired real commands headlessly.

status check       → ran     ✅ (allowed)
file list / logs   → ran     ✅ (allowed)
rm attempt         → denied  🚫 (deny)
read secret file   → denied  🚫 (deny)
trade order tool   → denied  🚫 (deny)

Allowed works, dangerous is blocked. Good. But there were remaining holes.

5. The trap: a whitelist doesn't block everything

If you take least privilege seriously, you must audit the "bypass paths." The holes I left:

  • A general interpreter is effectively arbitrary execution: allow python and you can do anything inside it, including deleting files. Denying rm is bypassed by python -c "os.remove(...)". Since it's a convenience (analysis power) vs. safety trade-off, I split and documented a "practical set (allowed)" and a "strict set (excluded)."
  • Blocking only the read tool leaks via the shell: even if you deny secret files in the "read tool," if a shell command like cat is allowed, it reads them anyway. You must consider tool-level deny and shell-command-level deny together.
  • Allowing file writes is indirect execution: allow file edits and you can modify a script or a scheduled-job file that runs later, bypassing the restrictions.

I judged these holes acceptable on the premise that the bot is owner-only (a sender whitelist blocks outsiders). If no outsider can drive it, the remaining risk is the owner's own mistakes / injection defense — which I left open to tighten to a stricter mode any time.

6. Lessons

  • Set permissions from data, not convenience. Instead of "open everything just in case," derive the necessary minimum from real usage logs and shrink the surface at no cost.
  • Deny first, and audit the bypass paths. allow alone isn't enough. The "universal tools" — interpreters, shells, file writes — can neutralize a whitelist.
  • Perfect least privilege and practicality are a trade-off. How far to tighten depends on "who can drive this bot." Strong access control → the practical set; otherwise → the strict set.
  • In headless, don't let "deny" become "stall." A missing permission must be silently denied and reported, so it doesn't freeze waiting for input.

FAQ

Q. Why not simply never give the bot dangerous tools?

Right — not exposing them at all is best where possible. But with a single bundled toolset (say, file + shell + search grouped together) you can't cleanly remove pieces, so exposing them but denying at the permission layer is the pragmatic route.

Q. Doesn't blocking even python cripple the bot?

That's exactly the trade-off. An interpreter is useful for heavy analysis/computation, but it's equally an arbitrary-execution window. If access control (who can drive the bot) is firmly a single owner, choose the practical set; to further lower injection risk, choose the strict set.

Q. Is least privilege alone enough to make the bot safe?

No — stack it in layers. This work assumed "a sender whitelist prevents outsiders from talking to the bot." Privilege reduction is the second layer on top of that, and the final line of defense is the account's own security (two-factor auth, etc.).

This post generalizes privilege-hardening work on a real, in-production personal AI agent. Specific hosts, paths, credentials, and tool names have been deliberately omitted.