Claude Code Permissions and Network Allowlists: How to Narrow Them per Project

To tighten tools and network access in a project with Claude Code permissions, put shared rules in .claude/settings.json under permissions.allow / ask / deny, and scope hosts with WebFetch(domain:…) plus (when needed) sandbox.network.allowedDomains / deniedDomains. Rules evaluate deny → ask → allow.

This article follows the official Configure permissions and Settings docs only: file locations, allow/deny lists, and how that pairs with Cursor rules. It is not a full project-settings tutorial and does not cover pricing.

Where are the settings files?

One-line answer: Share team policy in .claude/settings.json, keep personal overrides in .claude/settings.local.json, and store user defaults in ~/.claude/settings.json.

Official scopes:

  • Shared project: .claude/settings.json — commit for team permissions and hooks.
  • Project local: .claude/settings.local.json — you only; standing “don’t ask again” Bash/WebFetch approvals land here.
  • User: ~/.claude/settings.json — personal defaults for every project.
  • Managed: org-deployed settings — not overridable by you.

permissions.allow and additionalDirectories apply only after workspace trust. deny and ask restrict immediately. List keys merge across files; a deny anywhere blocks a matching allow.

How do you write allowlists?

One-line answer: Use Tool or Tool(specifier) entries in allow / ask / deny, and combine WebFetch(domain:…) with Bash denies and sandbox domain lists for network control.

{
  "permissions": {
    "allow": [
      "Bash(npm run lint)",
      "Bash(npm run test *)",
      "WebFetch(domain:docs.example.com)"
    ],
    "ask": [
      "Bash(git push *)"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Bash(curl *)",
      "Bash(wget *)",
      "WebFetch(domain:*.internal.example)"
    ]
  }
}

Contracts that matter:

  • Order: deny, then ask, then allow. A narrow allow never punches through a deny.
  • Bash *: wildcards anywhere; compounds split on && / | / ; and must match each subcommand.
  • WebFetch: WebFetch(domain:host) scopes hosts. A bare WebFetch deny removes the tool; WebFetch(domain:*) refuses per domain and also feeds the sandbox domain lists.
  • Bypass: if Bash can still run curl/wget, WebFetch alone is not a network fence. Prefer Bash network denies + WebFetch domain allows (+ sandbox allowlist).

With sandboxing, sandbox.network.allowedDomains / deniedDomains constrain hosts for shell children. strictAllowlist: true works from user, managed, or CLI --settings only—not from repo settings.json.

Use /permissions mid-session to inspect sources and edit rules.

How do you align with Cursor rules?

One-line answer: Cursor rules and CLAUDE.md describe intent; Claude Code permissions enforce what may run. Matching wording without a deny does not block anything.

LayerWhereRole
Intent / styleCLAUDE.md, Cursor rules / AGENTS.mdExplain build commands, banned habits, review norms
Hard boundary.claude/settings.json permissionsAllow / ask / deny tools, paths, domains
Runtime guardPreToolUse hooks (Claude) / Hooks (Cursor)Script exceptions rules can’t express

Practical alignment:

  1. Allow only docs/tracker hosts via WebFetch(domain:…).
  2. Deny Read(./.env*) and mirror paths in Cursor .cursorignore.
  3. Allow the same npm test / npm run lint commands your Cursor hooks expect.
  4. Put “no curl in prod” in permissions (Bash(curl *) deny + sandbox), not only in prose rules.

Permission rules are enforced by the Claude Code runtime, not the model. Prompts and CLAUDE.md do not rewrite permissions.

Wrap-up

To narrow permissions and network access: ① shared .claude/settings.json → ② allow/ask/deny and WebFetch domains → ③ keep Cursor rules for intent, permissions for enforcement. See Configure permissions and Settings.

Sources