← Back to Blog

Claude Code Field Guide: Settings and Security

TL;DR

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:

  1. User (~/.claude/settings.json) - your defaults
  2. Project (.claude/settings.json) - shared with team
  3. 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

DecisionRationale
Read(./) not Read()Scopes to current repo only; parent directories require approval
Deny Write/Edit for .env tooRead denial doesn't cascade to other tools
Bash deny for cat/head/.envPrevents bash-based file reading bypass
No bash whitelistClaude prompts for new commands; deny list blocks dangerous ones
Explicit gcloud deletesPattern 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:

  1. Read("./src/index.ts") → should work without prompting
  2. Read("../other-repo/file.ts") → should prompt for permission
  3. Read(".env") → should be blocked
  4. Bash(cat .env) → should be blocked
  5. Bash(gcloud functions delete test) → should be blocked
  6. git 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*