You are tired of approving the same harmless-looking command, so you click Always Allow. Maybe you even add it to the agent’s global permission list. It feels like a small convenience: one familiar tool, one repetitive prompt gone. But what exactly did you approve for every task that comes next?
Recently, while doing bug bounty research and working through a few CTFs, I kept running into the same pattern. An ordinary permission would reveal a much wider reach once I followed it all the way. After a while, I stopped treating each case as a clever trick. Every tool was doing what it was built to do. I had simply read the label too literally.
AI agents make this gap harder to ignore because they may know far more ways to use a tool than the person approving the request. Giving a permission to a trusted developer means trusting that developer’s judgment and intent. Giving the same permission to an agent also means accounting for every file, issue, and web page that can influence its next action. The agent may be trustworthy and still be manipulated into using a permission in a way you never intended.
In this post, I turned seven examples into a quiz. Decide what you are actually approving before reading each answer. I included a runnable example with each round, mostly Bash commands you can try yourself. Every example also has a plain-English explanation, so if the command line is not familiar, you can skip the code without missing the point.
Round 1: Is cat a read permission or a write permission?
On Unix systems, cat prints a file and echo prints text. Both look harmless until the shell gets involved. The shell can connect their output to a file, so cat source > destination copies a file and echo "new setting" > config creates or overwrites one. The > symbol means “put the output on the left into the file on the right.”
cat itself writes to standard output, which normally appears on the screen. When a command is passed to sh or bash, the shell decides where that output goes and can also connect or launch more commands. Here is a harmless example:
cat /etc/hosts > /tmp/hosts.copy
echo "permission demo" > /tmp/permission-demo.txt
sort -o /tmp/hosts.sorted /etc/hosts
uniq /etc/hosts /tmp/hosts.uniquesort and uniq reach the same result without shell redirection. sort has an -o option for the output file, while uniq treats its second filename as the output. An agent allowlist that approves commands only by name misses both routes: features supplied by the shell and write options built into the command.
Round 2: Does man only open documentation?
On many Linux systems, man normally opens software manuals, but man also lets the caller choose another program, called a pager, to display them. In this demo, the long quoted part replaces the normal viewer with a command that writes man-ran into a temporary file. The second line opens that file to prove it ran:
MANPAGER='sh -c "echo man-ran > /tmp/man-permission-demo"' man ls
cat /tmp/man-permission-demoThe output is man-ran. For an AI tool, allowing man therefore includes allowing the pager chosen by whoever formed the command.
Round 3: Is extracting an archive a read or a write?
When we say “open this archive,” it sounds like another kind of read. Extraction is permission to create an entire tree of files whose names and paths were chosen by whoever built the archive. The demo below performs three ordinary actions. It creates a file, packs its folder into an archive, and unpacks that archive somewhere else. The final line lists the file that tar recreated at the stored path docs/readme.txt:
mkdir -p /tmp/archive-demo/source/docs /tmp/archive-demo/output
echo "hello" > /tmp/archive-demo/source/docs/readme.txt
tar -cf /tmp/archive-demo/project.tar -C /tmp/archive-demo/source .
tar -xf /tmp/archive-demo/project.tar -C /tmp/archive-demo/output
find /tmp/archive-demo/output -type fAn archive is a list of write instructions. A safe extractor keeps those writes inside the intended folder, but it still creates every file it accepts there. A hostile archive may also contain paths or symbolic links that try to reach outside. When an extractor lets those writes escape, the bug is commonly called Zip Slip, or TarSlip when the archive is a tar file, and I recently even found one in Polyaxon’s archive-loading code.
Python’s tarfile documentation describes safer extraction filters and warns that none blocks every dangerous archive feature. Python 3.14 uses the safer data filter by default. The ordinary case is enough for this round: extraction is a write permission even when every file lands exactly where intended.
Round 4: Does installing a package also mean running it?
Installing a dependency sounds like downloading files. Package managers may also run scripts supplied by the package to compile components or prepare them. This means “install” can include “run code from this package.” The long printf line creates a tiny package description. Its postinstall instruction asks npm to print package-ran after installation:
demo=$(mktemp -d)
mkdir -p "$demo/package" "$demo/app"
printf '{"name":"demo","version":"1.0.0","scripts":{"postinstall":"echo package-ran"}}' \
> "$demo/package/package.json"
npm install --prefix "$demo/app" "$demo/package" --foreground-scripts --no-audit --no-fundThe output includes package-ran. The package requested a standard installation hook and npm ran it. You can disable these scripts in npm with npm install --ignore-scripts, although some packages may then fail because they legitimately need to compile native components during installation.
Some package managers use a more selective model. pnpm provides an approval command for dependency builds, while Bun runs lifecycle scripts only for dependencies it treats as trusted. The reason is simple: a postinstall script is package code running on your machine before you have even used the package. Before letting an agent add a dependency, I want to know whether I also trust the code that installation may execute.
Round 5: Does permission to use Docker also mean administrator access?
Imagine an AI coding agent needs to build an image, so you let it use the local Docker daemon. What did you grant: permission to run containers, or administrator access? On Linux with the normal root-owned Docker daemon, those are effectively the same answer because Docker can mount the host filesystem into a container and make it writable.
You do not need to recognize every option. The --mount line gives the container a writable view of the host, and the final line adds alice to the host’s sudo group. Run this only inside a disposable Debian or Ubuntu virtual machine:
# Replace alice with a test account that already exists on the VM.
docker run --rm \
--mount type=bind,source=/,target=/host \
ubuntu:24.04 \
chroot /host usermod -aG sudo alice
There is no container escape here. The root-owned daemon was asked for a writable view of the host and provided it. Docker warns that its group grants root-level privileges and documents that bind mounts are writable by default. On this setup, giving an AI agent Docker access gives it the same path to the host. Rootless Docker and remote daemons have different boundaries, so the daemon’s setup matters.
Round 6: What does permission to write to a Git repository mean?
Suppose an agent may edit a repository and run approved Git commands, but may not run arbitrary programs. Developers and build systems eventually run what repositories contain, so that boundary is weaker than it sounds.
The .git directory contains hooks, small programs Git runs automatically during actions such as commit. In this demo, the first four lines prepare a repository with an executable hook. Replacing the hook’s contents is then enough to choose what the next commit will run:
repo=$(mktemp -d)
git init -q "$repo"
touch "$repo/.git/hooks/pre-commit"
chmod +x "$repo/.git/hooks/pre-commit"
printf '#!/bin/sh\necho "hook ran" > /tmp/git-hook-demo\n' \
> "$repo/.git/hooks/pre-commit"
git -C "$repo" -c user.name=Demo -c user.email=demo@example.invalid \
commit --allow-empty -m demo
cat /tmp/git-hook-demo
Locking down .git does not remove the pattern. Committed files carry similar delayed authority. .github/workflows tells CI what to run, while Makefile, package.json, and .vscode/tasks.json can define local commands. A trusted action still has to happen later, but the repository write has already chosen what it will execute. The delay makes the permissions look separate.
Git can redirect authority without executing a hook. git remote set-url origin https://example.invalid/replacement.git changes where future pushes go. A permanent “Allow Git” decision can therefore affect code outside the local repository without changing a single source file.
Round 7: Is loading a model file the same as running code?
AI tools often load saved models, caches, and other Python objects. Some of those files use Python’s pickle format. Python’s documentation warns that untrusted pickle data must never be loaded because rebuilding an object can call Python functions. In this example, the object tells pickle to call print, which displays a message during pickle.loads:
import pickle
class Demo:
def __reduce__(self):
return print, ("code ran while loading",)
payload = pickle.dumps(Demo())
pickle.loads(payload)
The payload looks like stored data, but the format may also carry instructions for rebuilding Python objects. Loading a pickle-based model means trusting the functions it may call.
Command names make bad boundaries
Dividing commands into safe and dangerous lists sounds obvious, but a command’s effect depends on its arguments, environment, available files, and what happens next. cat printing to the screen is different from cat connected to a file. Writing a README is different from writing a Git hook. The command name alone cannot answer the permission question.
Research on the opposite approach shows the same weakness in command-name rules. Researchers tested 1,709 real-world command denylists and found that, depending on the operation, 69% to 98.6% of the denylists trying to block it still missed at least one command capable of performing it.
The paper does not measure allowlist failures, so those percentages do not apply directly to the examples above. It does show how many legitimate commands can reach the same effect. On the paper’s reference host, fully blocking file reads would have required adding an average of 217 command names, and most of the missed capabilities were documented in plain sight.
Drawing the right boundary is the hard part
The boundary can also move between systems. Read-only access to a project may expose an API token or SSH key, and that credential may carry write access somewhere else.
Approving every action does not solve the problem. After the tenth prompt, most of us start clicking automatically. Prompts should appear for meaningful effects and describe them clearly. “Change a Git hook that will run on the next commit?” gives me a decision I can make. “Allow Git?” does not.
For work that cannot be predicted command by command, a safe playground can move the boundary away from individual commands. Building that playground is harder than it sounds. A container is not much of a boundary if it can mount the host, reach cloud credentials, talk to production, or control the Docker daemon. The box has to restrict files, credentials, network access, and what survives after the task ends.
When that boundary is real, a sandbox lets the agent work automatically while I review what comes out. The paper used a related testing method: it ran candidate commands in a sandbox and inspected their effects instead of trusting their names.
My rule is easier to state than to implement: auto mode only inside a disposable environment whose access I have tested, with deliberate approval required to leave it. A sandbox reduces the consequences of a bad action. It does not make the agent trustworthy or remove the need to review the result.
The permission was always bigger
That escalated quickly, but only from our point of view. None of these permissions suddenly grew. Docker access already included writable host mounts. cat could already participate in a write. Repository access already covered files that trusted systems would later execute. The permission did not escape its box. We drew the box around the wrong thing.
Whenever an AI tool asks for a simple permission, I now ask what else the action enables on its happy path. That is the permission I am really approving.
What harmless-looking permission would you add as round eight?



What makes this worse than classic privilege creep is that the approval binds to the command's name, not its context — 'Always Allow' for curl in a scaffolding task quietly authorises exfiltration in every future task the agent runs. And it survives review because the audit trail shows a human consented; nobody logs what the human believed they were consenting to at the time. I've come to think per-task permission scoping with expiry is the only honest default, even though it reintroduces exactly the prompt fatigue that caused the click in the first place.