Blog

Claude Code security: what it can reach, what to lock down, what to watch

Claude Code security: what it can reach, what to lock down, what to watch

26 September 2026

Claude Code security comes down to one fact: the agent runs as the developer, with the developer's files, credentials and network, and every control that stops it from acting on those can be switched off by the developer in a JSON file. This article is for the CISO or security architect who has just learned that developers run it. It goes file by file: what it installs, what it reads, how permissions work and the ways they get weakened, the hooks and MCP servers that extend it, the one settings layer the security team owns, and what to monitor. Everything here is checked against Anthropic's own documentation, and the paths are the real ones.

What it installs and where

Claude Code is a terminal program. It writes a handful of files in the developer's home directory and in each project it works in, and those files are the whole configuration.

API keys and sign-in tokens go in the macOS Keychain where available, and behind file permissions on Windows and Linux. That part is done properly. The risk is not the agent's own credentials. It is everyone else's, sitting in the files the agent reads.

What it can read by default

In Manual mode the file tools read the working directory and any additional directories without asking, and a built-in set of read-only shell commands such as ls, cat and git status run without a prompt. The first read outside the working directories prompts. Writes stay inside the folder it was started in.

That boundary is exactly as wide as the developer makes it. Start it in a project and it reads that project's .env. Start it in the home directory and it reads the cloud CLI profiles, the SSH keys, the package registry tokens, and the .env of every project on the disk. One real laptop had forty-six credentials its agents could read, and none of them belonged to the agent.

The control is a Read deny rule: Read(./.env), Read(./secrets/**), Read(~/.aws/**). The documentation is explicit that these rules also cover cat, head, tail, sed and tee run through Bash, and the targets of shell redirections, and that a Read deny also blocks Edit and Write on the same path. Path rules anchor in ways that catch people out: /path in user settings means ~/.claude/path, not a project folder, so an organisation-wide rule uses the // absolute form or ~/.

How permissions work, and the three ways they get switched off

Permission rules come in three lists, allow, ask and deny, and a deny rule beats an allow rule everywhere: Bash(aws *) in deny blocks aws s3 ls even when a narrower allow names it. A bare tool name in deny, such as Bash, removes the tool from the model's context entirely. Rules match the command as written, so Bash(curl *) does not match /usr/bin/curl or sh -c 'curl ...', which is why the documentation points to sandboxing for enforcement that does not depend on command text.

Then there are modes, and this is where the security team should read slowly.

Mode What runs without asking
default (Manual) Read-only tools and read-only commands. Everything else asks.
acceptEdits File edits and mkdir, touch, rm, mv, cp, sed in the working directory.
plan Reads and read-only commands only.
auto Tool calls approved by a classifier model instead of the developer.
dontAsk Anything that would prompt is denied.
bypassPermissions Everything, including writes to .git and .claude.

With Claude Code v2.1.283 or later, auto mode is the built-in starting mode for interactive terminal and VS Code sessions. The person is no longer in the loop by default; a second model is. That is a reasonable engineering choice and a fact the risk register has to state.

Three things weaken it further, and the sensor finds all three on real machines.

  1. "Yes, and don't ask again." Each time a developer picks it for a command, Claude Code saves an allow rule to .claude/settings.local.json. Months of prompts produce a file full of Bash(git push *), Bash(npm *), Bash(curl *), granted once and never revisited. This is the standing permissions problem in its purest form.
  2. permissions.defaultMode: "bypassPermissions". The documentation limits this: the value does not take effect from project or local settings, only from user or managed settings. A developer's own ~/.claude/settings.json is enough.
  3. --dangerously-skip-permissions. A command-line flag that starts a session in bypass mode. The documentation's own guidance is a container or VM. On a laptop with the developer's SSH keys, it is an agent with no checks and full reach.

The fix for all three lives in one place. permissions.disableBypassPermissionsMode: "disable" in managed settings prevents bypass mode from being used at all, and disableAutoMode does the same for auto mode. Neither can be undone from a developer's files.

Hooks: persistence and the control point

A hook is a command Claude Code runs on an event: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, ConfigChange and about thirty others. It receives the event as JSON on standard input, and a PreToolUse hook can return a permission decision that allows or denies the call, or exit with code 2 to block it. Hooks run with the developer's environment.

That makes hooks two things at once. For an attacker, a hook in a project's .claude/settings.json, a plugin, or a skill's frontmatter is code that runs on every session and every tool call, in the developer's context, with nothing to click. Hooks are the persistence surface the OWASP LLM Top 10 calls data and model poisoning, and the sensor's hook_persistence rule exists for them.

For the security team, hooks are the only control that sees every call before it runs. Hook entries merge across settings levels rather than replacing each other, so a hook registered in managed settings runs alongside whatever the developer configured, and disableAllHooks in a user file cannot disable it. allowManagedHooksOnly goes further and blocks user, project, local and plugin hooks entirely.

MCP servers: the supply chain

MCP servers extend the agent with tools, and Claude Code loads them from three scopes: user and local scope in ~/.claude.json, project scope in .mcp.json at the repository root. A project's .mcp.json prompts for approval in interactive sessions before its servers are used, and the documentation is candid that in -p runs, the Agent SDK and cloud sessions, project servers load without prompting. A single setting, enableAllProjectMcpServers: true, removes the prompt everywhere.

The reference incident is the GitHub MCP prompt injection found by Invariant: an issue on a public repository carried instructions, the agent read it through its GitHub MCP server, followed them, and pulled private repository data into a public pull request. Anthropic's documentation says the same thing in its own words: servers that fetch external content expose you to prompt injection risk, and Anthropic does not security-audit MCP servers. What a security team wants is the list of servers on every machine, matched against advisories, and a hold on new ones until someone has looked. The MCP server security piece covers the server side; the organisation-side controls are allowedMcpServers with allowManagedMcpServersOnly, which turns the list into a lock, and deniedMcpServers, which an entry in any admin source applies.

CLAUDE.md and rules: the injection surface nobody audits

Every session starts by loading instruction files: CLAUDE.md from the working directory and every directory above it, CLAUDE.local.md, ~/.claude/CLAUDE.md, and every markdown file under .claude/rules/. They can import other files with an @path line, including files outside the project. Anthropic's documentation states plainly that Claude treats them as context, not enforced configuration.

That is the point for an attacker. A rules file committed to a shared repository, or pulled in through a dependency the developer opened in the agent, is text the model reads as instructions on every session. It is what a prompt injection looks like on a laptop when it does not need to wait for the agent to fetch anything. Claude Code shows a trust dialog for project files other people commit, and the sensor's prompt_injection rule reads these files for hidden instructions on every machine, because a dialog clicked once in March is not a control in September.

Managed settings: the one layer the security team owns

Everything above can be set by the developer. One layer cannot: managed settings, deployed as managed-settings.json in a system directory (/Library/Application Support/ClaudeCode/ on macOS, /etc/claude-code/ on Linux and WSL, C:\ProgramData\ClaudeCode\ on Windows), as an MDM profile under the com.anthropic.claudecode domain or the HKLM\SOFTWARE\Policies\ClaudeCode registry key, or as server-managed settings fetched from the claude.ai console. Managed settings apply above every other level, and a managed-settings.d/ directory next to the file lets several teams own their part of the policy without editing one shared file.

This is the mechanism the ZYBE sensor uses. Its installer writes a zybe.json into managed-settings.d/ that registers the sensor's hook on SessionStart, UserPromptSubmit, PreToolUse, PostToolUse and SessionEnd, in a directory a developer cannot quietly edit. A per-user registration happens as well, so a machine installed without root still reports, and the two together are why the sensor sees sessions that the developer's own settings would have hidden. The hook itself reads policy from a local file, never touches the network in the critical path, and fails open, because a security tool that stops a developer working gets uninstalled the same afternoon.

The documentation notes one honest limit: a developer with local admin rights can edit the managed file, which is why MDM redeploys it on a schedule and why the sensor's endpoint not reporting rule exists.

What to monitor

Three streams, all of which hooks provide and none of which a network proxy can.

Lock down first

  1. permissions.disableBypassPermissionsMode: "disable" in managed settings. One line, removes the worst case.
  2. Read deny rules for .env, ~/.aws/**, ~/.ssh/**, ~/.config/gcloud/** and whatever your estate keeps secrets in, in the // absolute form.
  3. allowManagedHooksOnly: true with your own hooks registered, so the only code that runs on every call is yours.
  4. allowedMcpServers plus allowManagedMcpServersOnly: true, seeded from the inventory of what teams actually use, and a review step for additions.
  5. Deny curl and wget in permissions.deny, and give the agent WebFetch(domain:...) rules for the hosts it needs. The documentation itself recommends this pairing.

Each of these is a finding the sensor already raises when it is missing, grouped by cause, with the fix beside it.

A worked example: one laptop

A senior developer's ~/.claude/settings.json sets defaultMode to bypassPermissions, added during a long refactor in the spring. Their .claude/settings.local.json in the payments repository holds fourteen allow rules, including Bash(curl *). The repository's .mcp.json names a community MCP server for the issue tracker, approved once. The project's .env holds a production database URL.

The sensor reports four findings on that machine: a safety control disabled, a permissive agent, an unreviewed component, and a readable production credential. The console groups them as one cause, the agent's reach, and shows the fix: the managed-settings line that ends bypass mode, the read deny rule that hides the credential, and a hold on the MCP server until someone reads it. Nothing in that list needs the developer to stop using Claude Code. It needs the security team to own the one file the developer cannot edit.

Frequently asked questions

Is Claude Code safe to run on company machines? It is as safe as the permissions it holds. Out of the box, in Manual mode, it reads the working directory freely and asks before it edits, runs a shell command that is not read-only, or fetches from the network. Every one of those checks can be switched off by the developer in a settings file or a command-line flag, and on current versions the default starting mode is auto, where a classifier reviews actions instead of a person. The security question is not whether the tool is safe but which of its controls are still on, on which machines.

What can Claude Code read on a developer laptop? Anything the developer's account can read. Its file tools read the working directory and additional directories without asking, and read-only shell commands such as cat and ls run without a prompt. That covers .env files in the project, and a developer who starts it in the home directory gives it the cloud CLI profiles and SSH keys too. Read deny rules such as Read(./.env) and Read(~/.aws/**) are the control, and they also cover cat, head, tail, sed and redirections.

How do you enforce Claude Code settings across an organisation? With managed settings: a managed-settings.json file in a system directory (/Library/Application Support/ClaudeCode/ on macOS, /etc/claude-code/ on Linux, C:\ProgramData\ClaudeCode\ on Windows), an MDM profile, or server-managed settings from the claude.ai console. Managed settings sit above every other level and nothing a developer sets in user, project or local files overrides them. The keys that matter are permissions.deny, permissions.disableBypassPermissionsMode, allowManagedHooksOnly, allowedMcpServers with allowManagedMcpServersOnly, and hooks.

Can a security team see what Claude Code is doing? Yes, through hooks. Claude Code runs a command you name on events such as SessionStart, UserPromptSubmit, PreToolUse and PostToolUse, and passes the tool name and its input as JSON on standard input. A PreToolUse hook can deny a call. Registered from managed settings, hooks cannot be removed by the developer, and disableAllHooks in a user file does not turn them off. That is how the ZYBE sensor records every session and tool call and blocks the ones a rule names.

Secure the agentic endpoint

Book a live demo →