← Back to blog
·6 min·Barta Gergő

How I Set Up Claude Code Permissions So It Can Work Without Me Watching

claude-code

I ran Claude Code with --dangerously-skip-permissions for months. Zero permission prompts, zero friction. Also zero guardrails. Every time Claude ran a command, there was this low-grade anxiety: what if it rm -rf's the wrong thing? What if it force-pushes to main?

It did happen. Claude ran git reset --hard during a confused debugging session and wiped uncommitted work. Luckily Claude wrote that code too, so I could tell it to look up the session transcript and recreate it. But the uncommitted changes were gone.

So I set up proper permission rules in settings.json. Three lists: allow (runs silently), deny (blocked, always), ask (prompts me for confirmation). Now Claude runs fully autonomous and I don't worry.

Where the rules live

Claude Code reads ~/.claude/settings.json for global rules and .claude/settings.json for per-project rules. The structure:

{
  "permissions": {
    "allow": ["pattern1", "pattern2"],
    "deny": ["pattern3"],
    "ask": ["pattern4"]
  }
}

Each pattern follows the format ToolName(argument-prefix). Bash(git status) matches exactly that command. Bash(git:*) matches anything starting with git. Bash(*) matches all bash commands (effectively bypass mode, but only for Bash). The official docs cover the full rule syntax and evaluation order.

My deny list: what Claude can never do

This is the important part. These rules can't be overridden by CLAUDE.md instructions or agent confusion. They're hard blocks.

"deny": [
  "Bash(git push --force *)",
  "Bash(git push -f *)",
  "Bash(git reset --hard *)",
  "Bash(git clean -f *)",
  "Bash(git credential *)",

  "Bash(rm -rf ~/)",
  "Bash(rm -rf ~)",
  "Bash(rm -rf /*)",
  "Bash(rm -rf /c/*)",

  "Bash(taskkill /F /IM node.exe)",
  "Bash(killall node)",
  "Bash(pkill node)",

  "Bash(sudo *)",
  "Bash(runas *)",
  "Bash(eval *)",
  "Bash(bash -c *)",
  "Bash(sh -c *)",
  "Bash(powershell -c *)",

  "Bash(wget *)",
  "Bash(nc *)",
  "Bash(socat *)",

  "Bash(reg add *)",
  "Bash(reg delete *)",
  "Bash(npm publish *)",
  "Bash(dd *)",
  "Bash(shred *)",
  "Bash(mkfs *)"
]

Categories:

Git destruction. Force push and hard reset are the obvious ones. I also block git credential because Claude doesn't need to touch auth tokens.

Filesystem nukes. rm -rf ~/ and rm -rf /* should be obvious. I also block my specific dev drive (/d/software_dev/*) because MSYS path mangling can turn /d into something unexpected.

Process kills that kill Claude. taskkill /F /IM node.exe kills every Node process on Windows: Claude Code, the dev server, the test watcher, all of it. Claude tried this multiple times to free a port during debugging. Each time I'd find multiple dead terminals with no processes running and have to restart everything. Now taskkill /F /IM node.exe, killall node, and pkill node are all in the deny list, and the CLAUDE.md tells it to find the specific PID with netstat -ano | findstr :<port> and kill only that one process.

Shell evaluators. eval, bash -c, sh -c, powershell -c. These let Claude construct arbitrary commands as strings, bypassing pattern matching on the allow/deny lists. Block them.

Network tools. wget, nc, socat have no legitimate use in my workflow. If Claude needs to fetch something, it uses curl against whitelisted domains.

My ask list: confirmation required

These are operations I want to know about before they happen, but don't want to block entirely.

"ask": [
  "Bash(git reset *)",
  "Bash(git stash drop *)",
  "Bash(git stash clear)",
  "Bash(git branch -d *)",
  "Bash(git branch -D *)",
  "Bash(git tag -d *)",
  "Bash(git checkout -b *)",
  "Bash(git switch -c *)",
  "Bash(del *)",
  "Bash(kill -9 *)",
  "Bash(find * -delete)",
  "Bash(find * -exec rm *)"
]

Branch creation is on the ask list because I want to know when Claude decides to branch. Soft resets (without --hard) are fine but I want to see them. File deletion with del or find -delete gets a prompt.

My allow list: what runs silently

This is the longest list. Around 100 patterns. The key groups:

"allow": [
  "Read(*)",
  "Edit(*)",
  "Write(*)",
  "WebSearch(*)",
  "WebFetch(*)",

  "Bash(git status)",
  "Bash(git log *)",
  "Bash(git diff *)",
  "Bash(git add *)",
  "Bash(git commit *)",
  "Bash(git branch *)",
  "Bash(git fetch *)",
  "Bash(git stash *)",
  "Bash(git checkout *)",
  "Bash(git merge *)",
  "Bash(git rebase *)",

  "Bash(npm run *)",
  "Bash(npm install *)",
  "Bash(npm test)",
  "Bash(npx *)",
  "Bash(node *)",
  "Bash(gh *)",

  "Bash(curl * *127.0.0.1*)",
  "Bash(curl * *localhost*)"
]

All file operations are allowed. All git operations except the destructive ones. Package managers, build tools, GitHub CLI. Curl is restricted to localhost and specific API domains I've whitelisted.

I also allow specific cleanup patterns:

"Bash(rm -rf *node_modules)",
"Bash(rm -rf */dist)",
"Bash(rm -rf */build)",
"Bash(rm -rf */.cache/*)",
"Bash(rm -f *.log)",
"Bash(rm -f *.tmp)"

Claude can clean build artifacts and caches but can't rm -rf anything outside those patterns.

The pattern matching catches edge cases

Bash(git push --force *) in the deny list overrides Bash(git:*) in the allow list even if you had it. Deny always wins. This means you can be broad in your allow list and surgical in your deny list.

The ask list sits between them. If something matches both allow and ask, the ask takes precedence. So Bash(git branch *) is allowed, but Bash(git branch -D *) prompts.

Additional directories

If Claude needs to read or search files outside your project, add them to additionalDirectories:

"permissions": {
  "additionalDirectories": [
    "C:\\Users\\youruser\\.claude\\usage-data",
    "C:\\Users\\youruser\\.claude\\skills"
  ]
}

Without this, Claude can't grep your skills directory or read files in other repos.

Why this beats bypass mode

With --dangerously-skip-permissions, Claude can do anything. With a proper setup, Claude can do everything it needs and nothing it shouldn't. The difference is I can walk away from the terminal.

I run multiple Claude agents in parallel through Mosaic Terminal. Five agents working simultaneously, all with these same permission rules. If one hallucinates a destructive command, it gets blocked. I don't need to watch all five screens.

The setup is a one-time cost. I keep settings.json in a git repo and sync it across machines. New project, same rules.


This is the permission layer behind my CLAUDE.md setup. Join the Discord for early access to Mosaic Terminal and a free weekly AI coding digest.