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 bareWebFetchdeny 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.
| Layer | Where | Role |
|---|---|---|
| Intent / style | CLAUDE.md, Cursor rules / AGENTS.md | Explain build commands, banned habits, review norms |
| Hard boundary | .claude/settings.json permissions | Allow / ask / deny tools, paths, domains |
| Runtime guard | PreToolUse hooks (Claude) / Hooks (Cursor) | Script exceptions rules can’t express |
Practical alignment:
- Allow only docs/tracker hosts via
WebFetch(domain:…). - Deny
Read(./.env*)and mirror paths in Cursor.cursorignore. - Allow the same
npm test/npm run lintcommands your Cursor hooks expect. - 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.