Use relative paths (./**) in global settings.json to scope file access to current repo, add comprehensive deny rules for sensitive files and destructive commands.
Part of the Claude Code Field Guide series.
The default permission prompts get annoying fast. Custom settings fix this while maintaining security.
Permission Philosophy
My approach:
- Allow: All reads, writes, edits within the current repo
- Deny selectively: Secrets, destructive commands, sensitive files
The key insight: use relative paths (./) to scope permissions to the current working directory only.
Gotchas I Learned the Hard Way
Deny doesn't cascade across tools
Read(./.env) denial does NOT block Write(./.env) or Edit(./.env). You must explicitly deny all three:
"Read(./.env)",
"Write(./.env)",
"Edit(./.env)"
Bash can bypass Read denials
Commands like cat, head, tail can read files even if Read() is denied. Add bash-level protection:
"Bash(cat .env:*)",
"Bash(head .env:*)",
"Bash(tail .env:*)"
Bash wildcard patterns are fragile
Bash(curl http://example.com:) won't match curl -X GET http://example.com because flags appear before the URL. The pattern matches from the start of the command.
For commands where flag order varies, deny the entire command: Bash(curl:).
Deny rules always take precedence
Order of evaluation: Deny → Ask → Allow. If something matches both allow and deny, deny wins.
Settings hierarchy
From lowest to highest precedence:
- User (
~/.claude/settings.json) - your defaults - Project (
.claude/settings.json) - shared with team - Local (
.claude/settings.local.json) - personal overrides, not committed
My Complete Configuration
This goes in ~/.claude/settings.json:
{
"permissions": {
"allow": [
"Read(./**)",
"Write(./**)",
"Edit(./**)",
"Read(~/.claude/**)",
"Read(~/.claude/skills/**)",
"Read(~/.claude/brands/**)"
],
"deny": [
"__ Sensitive files - all tool types __",
"Read(./.env)",
"Read(./.env.local)",
"Read(./.env.development)",
"Read(./.env.production)",
"Read(./.env.staging)",
"Read(./.envrc)",
"Read(./.secrets)",
"Read(./.secret)",
"Read(./.vault)",
"Read(./**/.env)",
"Read(./**/.env.local)",
"Read(./**/.env.development)",
"Read(./**/.env.production)",
"Read(./**/.env.staging)",
"Read(./**/.envrc)",
"Read(./**/secrets/**)",
"Read(./**/credentials.json)",
"Read(./**/.credentials/**)",
"Write(./.env)",
"Write(./.env.local)",
"Write(./.env.development)",
"Write(./.env.production)",
"Write(./.env.staging)",
"Write(./.envrc)",
"Edit(./.env)",
"Edit(./.env.local)",
"Edit(./.env.development)",
"Edit(./.env.production)",
"Edit(./.env.staging)",
"Edit(./.envrc)",
"__ Bash bypass protection __",
"Bash(cat .env:*)",
"Bash(cat .envrc:*)",
"Bash(head .env:*)",
"Bash(tail .env:*)",
"Bash(less .env:*)",
"Bash(more .env:*)",
"__ Git destructive __",
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git reset --hard:*)",
"Bash(git clean -f:*)",
"Bash(git checkout .:*)",
"__ Filesystem destructive __",
"Bash(rm -rf:*)",
"Bash(rm -r:*)",
"__ gcloud destructive __",
"Bash(gcloud compute instances delete:*)",
"Bash(gcloud container clusters delete:*)",
"Bash(gcloud dataproc batches delete:*)",
"Bash(gcloud dataproc clusters delete:*)",
"Bash(gcloud dataproc jobs delete:*)",
"Bash(gcloud functions delete:*)",
"Bash(gcloud iam service-accounts delete:*)",
"Bash(gcloud projects delete:*)",
"Bash(gcloud pubsub subscriptions delete:*)",
"Bash(gcloud pubsub topics delete:*)",
"Bash(gcloud run services delete:*)",
"Bash(gcloud scheduler jobs delete:*)",
"Bash(gcloud secrets delete:*)",
"Bash(gcloud sql instances delete:*)",
"Bash(gcloud storage rm:*)",
"Bash(gcloud workflows delete:*)",
"__ Other cloud destructive __",
"Bash(terraform destroy:*)",
"Bash(bq rm:*)"
],
"defaultMode": "default"
}
}
Note: The " comment " strings are just labels for organization - they're ignored as invalid patterns.
Design Decisions
| Decision | Rationale |
|---|---|
Read(./) not Read() | Scopes to current repo only; parent directories require approval |
| Deny Write/Edit for .env too | Read denial doesn't cascade to other tools |
| Bash deny for cat/head/.env | Prevents bash-based file reading bypass |
| No bash whitelist | Claude prompts for new commands; deny list blocks dangerous ones |
| Explicit gcloud deletes | Pattern gcloud delete is fragile; list each explicitly |
The .env Pattern
Claude can't read .env files, but it can read .env.example:
# .env.example (Claude can read)
DATABASE_URL=postgresql://user:pass@host:5432/db
API_KEY=your-api-key-here
# .env (Claude cannot read)
DATABASE_URL=postgresql://actual:credentials@prod:5432/db
API_KEY=real-key-abc123
Claude sees the variable names and structure, writes code that uses them correctly, but never sees actual secrets.
CLAUDE.md Reinforcement
Add a security section to ~/.claude/CLAUDE.md that reinforces the rules:
## Security
NEVER access: `.env`, `.envrc`, `.secrets`, `.vault`, `credentials.json`, `secrets/`
NEVER run: `git push -f`, `git reset --hard`, `rm -rf`, `terraform destroy`, `gcloud * delete`, `bq rm`
OK: `git push --force-with-lease`, `rm` (single file)
Per-repo overrides: `.claude/settings.json` or `.claude/settings.local.json`
This creates a belt-and-suspenders approach: settings.json enforces the rules programmatically, CLAUDE.md reminds Claude of the intent.
Per-Repo Overrides
If a specific repo needs different rules, create .claude/settings.json:
{
"permissions": {
"allow": [
"Read(./.env.example)",
"Bash(gcloud functions delete:*)"
]
}
}
Project settings override global settings for that repo only.
Verification
Start a new Claude session and test:
Read("./src/index.ts")→ should work without promptingRead("../other-repo/file.ts")→ should prompt for permissionRead(".env")→ should be blockedBash(cat .env)→ should be blockedBash(gcloud functions delete test)→ should be blockedgit push --force-with-lease→ should work (not in deny list)
Dependency Management
Claude's training data has a cutoff date. It may suggest older package versions or deprecated APIs—or worse, hallucinate packages that don't exist.
Evaluate packages before accepting:
"Before installing, tell me about this package: How long has it existed?
Is it actively maintained? How many weekly downloads? Are there more
established alternatives?"
Watch for hallucinated packages. LLMs sometimes suggest package names that don't exist. Attackers have exploited this by creating malicious packages with names that LLMs commonly hallucinate. Always verify a package exists and check its provenance before installing.
Next: Environment and Efficiency
Back to Claude Code Field Guide*