Permission modes
| Mode | What it does |
|---|---|
| Default | Asks the first time each kind of edit or command comes up. |
| Accept edits | File edits go ahead without asking. Shell commands still ask. |
| Plan | Claude reads and plans but doesn't change anything. |
| Bypass permissions | Nothing asks. This is --dangerously-skip-permissions, covered in the YOLO mode guide. |
Press Shift+Tab to move between modes in a session, or set a default with permissions.defaultMode in settings.
Where settings live
~/.claude/settings.json: your rules for every project..claude/settings.json: the project's rules, committed for the team..claude/settings.local.json: your rules for this project only.
Organisations can also push managed settings that users can't override. Type /permissions in a session to see every rule in effect and which file it came from.
Rule syntax
A rule is a tool name, optionally with a pattern in brackets. Rules go in three lists: allow runs without asking, ask always asks, deny never runs. Deny wins over ask, and ask wins over allow.
| Rule | Matches |
|---|---|
Bash(npm run test:*) | Shell commands that start with npm run test |
Bash(git push:*) | Any git push |
Read(./.env) | Reading .env in the project root |
Read(./secrets/**) | Reading anything under secrets/ |
Edit(./migrations/**) | Editing files under migrations/ |
WebFetch(domain:example.com) | Fetching pages from one site |
mcp__github | Every tool from the MCP server named github |
A starting point
{
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run test:*)",
"Bash(git status)",
"Bash(git diff:*)"
],
"ask": [
"Bash(git push:*)",
"Bash(git reset:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Bash(rm -rf:*)",
"Bash(curl:*)",
"Bash(sudo:*)"
]
}
}Is there a .claudeignore?
Claude Code doesn't use an ignore file of its own. Deny rules for Read are how you keep it out of files. It does respect .gitignore when searching, but a file being git-ignored doesn't stop Claude from opening it by name.
Where rules stop
- A
Readdeny rule applies to Claude's file tools. A shell command that reads the same file, such ascat .env,grep KEY .envorsource .env, is aBashcall and isn't matched by it. Bashrules match the start of the command.Bash(cat:*)doesn't matchhead .env,less .envor a Python one-liner, and you can't list every program that can read a file.- Rules can't see environment variables.
printenvandecho $STRIPE_KEYread nothing from disk. - In bypass mode, allow and ask rules stop mattering. Deny rules still apply.
For those cases you need something that looks inside the command or limits the process itself: a `PreToolUse` hook, or the sandbox with its own file rules.
Filling the gaps with Rollcage
The Rollcage Guard is a hook that checks the content of every tool call, so it catches cat .env.local, printenv and python3 -c "import os; print(os.environ)" alike, and applies the same rules in Codex, Cursor and Gemini CLI. Keep your deny rules as well: the two work together.
Questions
How do I stop Claude Code reading my .env file?
Add deny rules for Read(./.env) and Read(./.env.*). That covers Claude's file tools. To cover shell commands as well, turn on the sandbox or add a PreToolUse hook that inspects Bash commands.
What does allowedTools do?
The --allowedTools and --disallowedTools flags set allow and deny rules for one run, using the same syntax as settings.json. They're mostly used in scripts and CI.
Do deny rules still work with --dangerously-skip-permissions?
Yes. Bypass mode removes the prompts, not the deny list. Hooks keep running too.
Why does Claude keep asking for the same command?
The allow rule probably doesn't match. Rules match the beginning of the command, so a leading cd or an environment variable in front changes it. Check /permissions to see what's in effect.