When to Skip Permissions

Permissions, Sandboxes, and Sharp Edges

permissionssandboxplan modeaudit logsdevcontainer

There’s a flag called --dangerously-skip-permissions. The name is the warning label. People still type it on their main machine, watch their .env get rewritten, and learn the hard way. This chapter is so you don’t.

I’ve watched smart engineers turn a perfectly fine afternoon into a recovery operation because they wanted to “just let it run.” The agent is fast. Fast agents on unprotected filesystems is how you discover, in real time, which of your repos still had a hardcoded production token from 2024.

Permissions are not bureaucracy. They are the steering column collapse zone of agentic coding. You build them once, and then you can drive at speed.

The permission model in 60 seconds#

By default, every tool call that touches the world — Edit, Write, Bash, WebFetch, tools — shows a diff or command preview and asks approve? before executing. You get four choices:

The model never silently writes to disk in default mode. Every destructive thing is gated. Reads of files inside your working directory are not gated, but anything that mutates state is. That’s the contract. Memorize it.

When you pick option 3, you’re teaching your future self to trust this pattern. Pick carefully. “Always allow Bash(rm -rf*)” is the kind of muscle-memory mistake that ends weekends.

screenshot
The permission prompt
`Bash(npm test)` with the 'always allow' options visible.
id: 15-permissions-1 · drop 15-permissions-1.png into public/screens/

Permission granularity — what you can scope#

Settings live at three levels: managed (org), user (~/.claude/settings.json), and project (.claude/settings.json checked into the repo). Rules are evaluated deny -> ask -> allow. Deny always wins. The first matching rule resolves the question.

The pattern syntax looks like this:

{
  "permissions": {
    "allow": [
      "Bash(npm test*)",
      "Bash(npm run build*)",
      "Edit(src/**/*)",
      "Read(/etc/hosts)"
    ],
    "deny": [
      "Bash(rm -rf*)",
      "Bash(git push origin main)",
      "WebFetch",
      "Edit(.env*)"
    ]
  }
}

A few things that bite people the first week:

Order matters across files too. If your org’s managed settings deny Bash(git push*), your project settings cannot allow it. Deny is sticky upward.

Blast-radius simulator
Where are you running?
Flags & rules in effect
Filesystem (mutating)
Approval gates protect every Edit and Write.
green
.env / secret files
`Edit(.env*)` denied. Agent cannot rewrite secrets.
green
git push origin main
Without explicit deny, agent could push to main.
yellow
rm -rf
`rm -rf` denied. Recursive deletes blocked.
green
Network egress
WebFetch and curl are reachable. Limit if not needed.
yellow
Production credentials
Approval gates help, but the creds are still sitting on disk.
yellow

—dangerously-skip-permissions — what it actually does#

The flag (sometimes surfaced as “bypass mode”) disables every gate. Edit, Write, Bash, WebFetch, MCP — everything runs without asking. The model can still refuse on its own judgment, but the safety layer between model and your filesystem is off.

The use case Anthropic intended is narrow: ephemeral, sandboxed environments where the cost of “agent did something stupid” is “rebuild the container.” Docker, GitHub Codespace, throwaway VM, CI runner with no secrets. Places where the blast radius is small and reversible.

When you should not use it:

There’s a sibling setting, permissions.disableBypassPermissionsMode: "disable", that locks the flag out at the user or managed-settings level. If you run a team, set it in managed settings and stop having the argument.

The right way to use it (when you must)#

The pattern is: spin up a first, then let the agent off the leash inside it.

# Option 1: Docker, no network, working dir mounted read-write
docker run --rm -it -v "$(pwd):/work" -w /work \
  --network none \
  node:20 bash -c "npm i -g @anthropic-ai/claude-code && claude --dangerously-skip-permissions"

# Option 2: GitHub Codespaces, e2b, Daytona, or any prebuilt cloud sandbox
# Option 3: A dedicated VM where prod credentials have been revoked and rotated

Rules of the sandbox:

That’s the whole recipe. The cost of doing this once and templating it is one afternoon. The cost of not doing it is a story you’ll tell at conferences.

Plan mode — preview without execution#

claude --plan (or the in-session toggle) makes the agent describe what it would do without doing it. No Edit, no Write, no Bash side effects. It reads, it thinks, it tells you the plan.

I use plan mode for:

It’s the closest thing to “show me the diff for the whole job before you start.” Read it, push back where it’s wrong, then run for real.

Tool allow-lists in CI#

In a GitHub Action you don’t have a human approving every step. So you scope at launch:

- name: Auto-fix tests
  run: |
    export ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}
    claude --print "Run npm test, fix any failing tests, commit, push" \
      --allowed-tools "Bash(npm test*),Bash(git*),Edit(src/**/*)" \
      --disallowed-tools "Bash(rm*),WebFetch,Edit(.env*)"

Pair --allowed-tools (the launch-time allowlist) with deny rules in ~/.claude/settings.json or a checked-in .claude/settings.json. Belt and suspenders. The CLI flags scope a single run; the settings files are the floor that no run can sink below.

Devcontainer recipe#

Anthropic publishes a devcontainer config for safe agent execution. Minimal version:

{
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",
  "postCreateCommand": "npm install -g @anthropic-ai/claude-code",
  "containerEnv": {
    "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}"
  },
  "features": {
    "ghcr.io/devcontainers/features/git:1": {}
  },
  "remoteUser": "node"
}

Open in VS Code, “Reopen in Container,” and run claude --dangerously-skip-permissions inside. The container is the blast wall. Your laptop never sees the agent’s file operations.

For my own newsletter publishing pipeline I run a near-identical setup — the devcontainer has scoped credentials for the publishing API and nothing else. If the agent loses its mind, the worst it can do is mangle drafts.

What about Cowork?#

’s sandbox is enforced by default. Bash runs in a managed Linux VM, no host filesystem access except folders you explicitly attach, allowlisted network. You don’t need a --dangerously-skip-permissions equivalent because the surface is already isolated.

The price you pay: the agent can’t reach into your laptop’s file system unless you mount a folder explicitly. That’s not a bug, that’s the contract. If you wanted host access, you wanted Claude Code locally.

Common permission patterns by job#

Greenfield repo. Allow Edit(**), allow Bash(npm*) and Bash(git*) minus git push*, deny WebFetch unless you actually need it. Let the agent fly inside the box. Don’t let it leave.

Production-adjacent repo. Deny Edit(.env*), deny Edit(**/secrets/**), deny Bash(rm*), deny Bash(curl*) and WebFetch. Allow only the test runner, the linter, and reads. The agent can analyze, suggest, and prepare PRs; it cannot reach out to the world or rewrite credentials.

Documentation-only repo. Allow Edit(*.md), Edit(*.mdx), Edit(docs/**). Deny everything else. Boring repos deserve boring permissions. The agent has no reason to run Bash in a docs repo.

Codex-style 24/7 monitor. Runs in a container with --dangerously-skip-permissions, mounted read-only on the production data, outbound network restricted to the SaaS APIs it actually needs (Stripe, the CRM, the analytics warehouse). Belt, suspenders, parachute. The flag is fine here because the container is the cage.

screenshot
A real `~/.claude/settings.json` permissions block from my main machine
Names redacted as needed.
id: 15-permissions-2 · drop 15-permissions-2.png into public/screens/

Audit logs — your seatbelt#

Every tool call is logged. Default location is ~/.claude/logs/ (with subdirectories per project). When something feels off — “did the agent really push to main?” — the audit log answers it before your blood pressure does.

Grep the logs the same way you grep anything else. If a CLI subcommand like claude logs is exposed in your version, use it; otherwise the raw files are fine. Look for Bash(, Edit(, WebFetch( and the timestamp on the call you’re worried about. The log has the exact arguments, exit code, and which session it came from.

I check logs about once a week on machines where I’ve allowed broad permissions. Five minutes, no surprises, peace of mind. If you ever do see a surprise, that’s your signal to tighten a rule and move it from allow to ask.

The closing rule#

On your main machine, never skip permissions. In a sandbox, you don’t need to ask. The line between the two is a checklist, not a vibe. Write the checklist down. Tape it to the monitor if you need to. I’d rather you look paranoid than rebuild your dotfiles from a backup that’s three weeks stale.

Watch alongside
Prompts for Hardening & Security
Spotted something wrong, missing, or sharper? Email Vlad with feedback on this chapter →
Stay close

Edition 3 lands when this list says it does.

No course. No paywall. Operator playbooks weekly. 10K+ subscribers.