~/.claude/settings.json; a terminal (bash or PowerShell); one throwaway project directory for testing; a text editor.claude --help — check the settings reference in the docs.Claude Code talks to the outside world through several separate channels. Blocking one does not block the others — that is the most common reason a “private” setup still leaks URLs.
What to do: confirm the CLI is installed, then inventory the four layers that decide what can leave your machine: user settings, project settings, CLI flags, and MCP servers (MCP — a plugin protocol that lets Claude Code call external tools and servers).
claude --version
ls -la ~/.claude/
cat ~/.claude/settings.json 2>/dev/null || echo "no user settings file yet"
Example: on a fresh install you will typically see a version line, a ~/.claude/ directory containing settings.json or nothing at all, and the message no user settings file yet. Both outcomes are fine — you create the file in Step 2.
Why: if the file does not exist, every rule you write later has to be created from scratch; if it does exist, you must merge, not overwrite, or you will silently drop existing permissions.
Now map the channels with this table — read it once, and you know exactly which switch to flip:
| Channel | What can leave the machine | Control |
|---|---|---|
| Model requests | prompt text, file contents you attach | `ANTHROPIC_BASE_URL` pointed at your own endpoint |
| `WebFetch` tool | the URL and the page it pulls | deny rule `WebFetch` |
| `WebSearch` tool | your search query | deny rule `WebSearch` |
| `Bash` | `curl`, `wget`, `pip install`, `git push` | deny rules `Bash(curl:*)`, `Bash(wget:*)` |
| Telemetry and crash reports | usage events, error traces | `DISABLE_TELEMETRY`, `DISABLE_ERROR_REPORTING` |
| MCP servers | whatever that server is built to send | remove or review `mcpServers` |
How to check: run the inventory checklist below. Every box must be reproducible, not assumed.
claude --version prints a version~/.claude/settings.json path is known and readableclaude --help lists the tool-restriction flag you intend to use (names differ per release)If you plan to keep prompts entirely off hosted APIs, the endpoint part is covered in How to connect a local LLM to Claude Code — set that up after finishing this guide.
What to do: create or open the user-level settings file. On macOS and Linux it is ~/.claude/settings.json; on Windows it is %USERPROFILE%\.claude\settings.json. Create it with valid JSON before adding any rules.
mkdir -p ~/.claude
printf '{}\n' > ~/.claude/settings.json
python3 -m json.tool ~/.claude/settings.json
Why: permission rules live inside a JSON object. Editing an empty or malformed file is the single biggest source of “I added the rule and nothing happened”. Validating with json.tool takes one second and removes that whole failure class.
Example: the command above prints {} — that is your green light. If it prints Expecting property name enclosed in double quotes, the file has a trailing comma or a comment (// is not valid JSON) and Claude Code will fall back to defaults.
How to check: open a session in a test directory and run the /permissions slash command. The panel must open and list your current allow/ask/deny rules — an empty list is expected at this point.

What to do: put the actual restrictions into ~/.claude/settings.json. This is the key step: it defines the deny list for the tools that reach third-party sites, and the environment block that turns off telemetry.
{
"permissions": {
"deny": [
"WebFetch",
"WebSearch",
"Bash(curl:*)",
"Bash(wget:*)"
]
},
"env": {
"DISABLE_TELEMETRY": "1",
"DISABLE_ERROR_REPORTING": "1",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
Why it works: the deny array is evaluated before the model can call a tool, so the request never becomes an HTTP call. "WebFetch" without parentheses blocks the tool entirely; the parenthesised form is the scoped version. The env block injects variables into every session, so you do not depend on your shell profile.
If you want a narrow allowlist instead of a blanket block — for example one documentation domain — replace the first entry with a scoped rule and keep the rest:
"deny": [ "WebSearch", "Bash(curl:*)", "Bash(wget:*)" ],
"allow": [ "WebFetch(domain:docs.anthropic.com)" ]
Example: with the allowlist version, “fetch docs.anthropic.com and summarise” succeeds, while “fetch https://example.com” is refused. That is exactly the behaviour you want to demonstrate.
For a single run without touching the file, use the CLI restriction flag:
claude --disallowedTools "WebFetch,WebSearch" -p "summarise README.md without downloading anything"
How to check: python3 -m json.tool ~/.claude/settings.json must print the formatted object without errors, and /permissions inside a new session must now show your four deny entries.
Two more things to inspect while you are here: run /mcp to list connected servers, and delete any entry under mcpServers in the settings file that you did not configure yourself. Then confirm with your provider’s own control panel that training on your conversations is off — the walkthrough is in How to opt out of AI training on your data.

What to do: attack your own setup with three prompts that would each leak data if a rule were wrong.
claude -p "Fetch https://example.com and give me the page title"
claude -p "Search the web for my email address and tell me where it appears"
claude -p "Run: curl -s https://example.com | head -5"
Why: a rule that exists in the file but is not loaded looks identical to a rule that works — until you try. Passive checks prove the file parses; only an active attempt proves the gate closes.
Example: all three commands must stop before any network call and return a refusal naming the tool, for instance WebFetch denied, WebSearch denied, and a Bash denial for curl. Success looks like three refusals and zero page content. A single line of real HTML in the output means the rule did not load.
How to check: confirm the environment block actually reached the session. Inside an interactive session type:
!env | grep -E 'DISABLE_TELEMETRY|DISABLE_ERROR_REPORTING|CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'
Expected output — three lines, one per variable:
DISABLE_TELEMETRY=1
DISABLE_ERROR_REPORTING=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
Finish with the checklist: - [ ] all three fetch attempts refused, - [ ] three environment variables visible inside the session, - [ ] /permissions lists the deny rules, - [ ] /mcp shows only servers you recognise.
Problem. The settings file is ignored and no rules load.
Error: Invalid settings file ~/.claude/settings.json: Unexpected token } in JSON// comment left in the file.python3 -m json.tool ~/.claude/settings.json; if it fails, delete the last comma and re-run until it prints the object, then restart the session.Problem. WebFetch is refused even for the domain you allowlisted.
Permission to use WebFetch has been denied."WebFetch" entry in deny takes precedence over any allow rule for the same tool — both entries exist in the file."WebFetch" string from deny, keep only "WebFetch(domain:docs.anthropic.com)" in allow, then re-test with the allowlisted URL and a blocked one.Problem. Telemetry variables are set in the shell but absent in the session.
env | grep DISABLE_TELEMETRY returns exit status 1 and prints nothing.env block of ~/.claude/settings.json instead of ~/.bashrc, then restart the session; launch paths differ per client, so check how the desktop app is configured if you use it.You now have a validated user-level settings file that denies WebFetch, WebSearch and shell downloaders, plus a session-level environment block that disables telemetry and error reporting. Next, replicate the deny list into .claude/settings.json inside shared repositories so teammates inherit it, and route model calls to your own endpoint if prompts must not leave your network.
Does denying WebFetch stop Claude Code from reaching the internet at all?
No. It closes one tool. Bash can still run curl or wget unless you deny those command patterns too, which is why Step 2 lists all four rules together.
Where do I put the settings — user level or project level?
Put network restrictions in ~/.claude/settings.json so they apply everywhere, and put project-specific entries in .claude/settings.json inside the repository. When both exist, verify with /permissions which set is actually active in your session.
Can I allow one documentation site and block everything else?
Yes. Keep "WebFetch(domain:docs.anthropic.com)" in allow and remove the bare "WebFetch" entry from deny; the scoped delete/allow form is what makes per-domain control work.
Does this stop my prompts from being sent to the model provider?
No. Permission rules govern tools, not the model API call. To keep prompt text inside your network, point Claude Code at your own endpoint as described in the local-model guide and confirm that endpoint is the one in use.