Skip to content
Personal blog. Opinions are my own. Always refer to official documentation.
Back to posts
Security

Locking Down Claude Code: A Practical Guide with Simple Examples

EL
Eric Lam
April 7, 2026 · 11 min read

Claude Code can be made much safer, but only if you understand what each control is actually doing.

A lot of teams get confused because the names sound similar:

They are not the same thing.

A simple way to think about them is this:

If you only remember one thing from this article, remember this:

CLAUDE.md is guidance. Permissions, sandboxing, hooks, and managed settings are enforcement.

That is the difference between “please behave safely” and “this action is blocked.”


1) Start with the right mental model

The biggest mistake is treating CLAUDE.md as if it were a security control.

It is useful, but it is not a hard boundary.

For example, you could write this in CLAUDE.md:

Never read .env files. Never use curl. Never edit files outside this project.

That may influence Claude’s behavior, but it is still just an instruction.

If you actually want to enforce those restrictions, you need the real controls:

Simple example

Imagine telling an intern:

“Please do not open the finance folder.”

That is like CLAUDE.md.

Now imagine locking the finance folder, disabling copy access, and requiring manager approval to enter.

That is what real enforcement looks like.


2) Understand permissions first

Permissions are the first layer of control.

They decide which Claude Code tools are allowed to run.

For example, if you deny this:

"Read(./.env)"

Claude’s built-in file-reading tools should not be allowed to read that file.

That is useful, but it is not the whole story.

If Bash is available, a shell command could still try this:

cat .env

That is why permissions alone are not enough in a sensitive environment.

What permissions do

Permissions control Claude Code tools such as reading files, editing files, running Bash, or fetching URLs.

What permissions do not do

Permissions do not magically create an operating-system boundary. If Bash is allowed and not properly sandboxed, the OS may still allow a subprocess to try something dangerous.

Simple example

Think of permissions as the front desk in an office.

They can say:

That is useful.

But if there is an open back door, the front desk is not enough.

That “back door” is why sandboxing matters.


3) Understand the difference between permission modes and permission rules

This is one of the easiest places to confuse readers, so let’s make it explicit.

Permission modes

A permission mode sets the overall approval behavior for the session.

Examples of modes include:

Permission rules

A permission rule applies to a specific tool or pattern.

Examples of rule arrays are:

So:

Simple example

Think of it like airport security.

That is why these two lines do different jobs:

"defaultMode": "dontAsk"

and

"deny": ["Read(./.env)"]

The first changes the overall approval style. The second blocks one specific kind of action.


4) Build a secure baseline first

A strong baseline should do four things:

  1. explicitly deny access to sensitive files
  2. enable sandboxing
  3. fail closed if sandboxing is unavailable
  4. disable unsandboxed escape hatches unless you truly need them

Here is a clean baseline configuration:

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "defaultMode": "dontAsk",
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(~/.aws/credentials)",
      "Read(~/.ssh/**)",
      "Bash(curl *)",
      "Bash(wget *)",
      "Bash(rm -rf *)"
    ]
  },
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "autoAllowBashIfSandboxed": true,
    "allowUnsandboxedCommands": false,
    "excludedCommands": ["docker *"],
    "filesystem": {
      "denyRead": ["~/.aws/credentials", "~/.ssh"],
      "denyWrite": ["/etc", "/usr", "~/.bashrc", "~/.zshrc"],
      "allowRead": ["."]
    },
    "network": {
      "allowedDomains": ["github.com", "*.npmjs.org", "registry.yarnpkg.com"]
    }
  }
}

What this config means in plain English

This config says:

Simple example

Without this baseline, Claude might try to:

With this baseline, those actions are either denied directly or constrained by sandbox policy.

That is the difference between “probably safe” and “safer by design.”


5) Why you need both permissions and sandboxing

Many readers understand one of these layers, but not the relationship between them.

Here is the clearest way to explain it:

You usually want both.

Simple example

Suppose you block this:

"Read(./.env)"

Good.

That stops Claude’s built-in file-reading tool from opening .env.

But what if Bash tries this?

cat .env

Now the file tool restriction is not enough by itself.

That is where sandboxing comes in.

So a good explanation for readers is:

Permission rules stop Claude tools from asking for dangerous actions. Sandboxing stops Bash from doing dangerous things even if Bash is invoked.

That sentence is much easier to remember than a long abstract explanation.


6) Treat Bash wildcard rules as weak filters, not strong security

A common mistake is to write a Bash rule that looks strict and then trust it too much.

For example:

"Bash(curl https://example.com/*)"

That looks safe at first glance.

But Bash matching is not a perfect security boundary. Flags, variables, wrappers, redirects, or alternative command structures can make string-based matching weaker than it appears.

Simple example

A reader might think this rule means:

“Only curl to example.com is allowed.”

But in practice, string matching can be trickier than that. A command can be restructured in ways that are not obvious if you rely only on pattern matching.

Better guidance

Use Bash rules as one filter, not the only one.

A stronger setup is:

In plain English:

Do not trust a single text pattern to carry your whole security model.


7) Sandboxing is powerful, but it is not magic

Sandboxing is one of the most valuable protections in Claude Code because it creates a real boundary around Bash and its child processes.

But you should not describe it like this:

That language is too absolute.

A much better sentence is:

Sandboxing materially reduces risk, but it does not eliminate risk.

Simple example

Imagine a workshop with locked tool cabinets.

That is much safer than leaving every dangerous tool on the table.

But if you still leave the windows open, give out master keys, and let people walk in and out freely, the workshop is not “perfectly secure.”

That is how sandboxing should be described: a major protection, not a magic shield.


8) Understand autoAllowBashIfSandboxed

This setting is easy to misunderstand.

When autoAllowBashIfSandboxed is enabled, sandboxed Bash commands can run without the usual approval prompt. This behavior works independently of your overall permission mode.

That does not automatically mean the system is unsafe.

It means the protection is coming from the sandbox boundary rather than from a prompt.

Simple example

Without autoAllowBashIfSandboxed:

With autoAllowBashIfSandboxed:

So the safety question becomes:

“How strong is my sandbox?”

not just:

“Did I see a prompt?”

That is an important mindset shift.


9) Hooks are where policy becomes dynamic

Permissions are static. Hooks are dynamic.

That is why hooks are so useful.

Hooks let you inspect an action at runtime and decide what should happen next. Claude Code supports multiple hook events, including PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, and PermissionRequest.

For security policy, PreToolUse is often the most important one because it lets you check a tool call before it runs.

Simple example

Suppose your rule is:

Claude may edit files inside this repo, but not outside it.

A static deny rule may be awkward here, especially if the path changes.

A hook can inspect the exact path at runtime and decide.

That is much more flexible.

Here is a safer example:

#!/usr/bin/env bash
set -euo pipefail

input="$(cat)"
file_path="$(jq -r '.tool_input.file_path // empty' <<<"$input")"
project_dir="${CLAUDE_PROJECT_DIR:?}"

[[ -z "$file_path" ]] && exit 0

target="$(realpath -m "$project_dir/$file_path")"
root="$(realpath "$project_dir")"

case "$target" in
  "$root" | "$root"/*)
    exit 0
    ;;
  *)
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Writes outside the project root are blocked"
      }
    }'
    exit 0
    ;;
esac

What this hook is doing

It checks where Claude is trying to write.

Why this is better than a naive check

A weak script might just look for whether the path “starts with” the project directory.

That can be brittle.

This hook is better because it normalizes the path first.

Simple example

A bad path check might be fooled by things like:

A stronger hook resolves the real path first and then decides.

That is the kind of example that makes the benefit obvious to readers.


10) Write hooks defensively

Hooks are powerful, but they are also privileged.

If your hook runs with your full user permissions, then a badly written hook can create a new problem instead of solving one.

Good habits for hooks

Simple example

A hook is like a security guard with a master key.

That is useful only if the guard follows good procedure.

A careless guard can make the building less safe, not more safe.

That is exactly how readers should think about hooks.


11) Be careful when discussing MCP

MCP is another place where articles often become vague.

The safest explanation is:

So you should not assume that Bash sandboxing is what governs MCP behavior.

Simple example

If you sandbox Bash, that helps when Claude runs shell commands.

But an MCP server is not just “Bash in disguise.”

So the correct reader takeaway is:

Control MCP with permissions and managed MCP policy, not by assuming the Bash sandbox covers it.

That is clearer and less misleading.


12) Managed settings are what make enterprise policy real

For a solo developer, local settings may be enough.

For a company, they usually are not.

If a user can simply override the controls, then the policy is not really policy.

That is why managed settings matter in enterprise deployments.

Managed-only controls can prevent local settings from weakening centrally defined rules. Examples include:

Simple example

Imagine a company says:

“Nobody may connect unapproved MCP servers.”

If each user can ignore that locally, then the rule is only a suggestion.

Managed settings are what turn that suggestion into enforcement.

That is why this layer matters so much in real deployments.


13) Avoid absolute claims in security writing

One of the fastest ways to weaken a security article is to overstate what a control does.

Avoid phrases like:

Those phrases usually collapse under edge cases.

A stronger writing style is:

Simple example

Compare these two sentences:

Weak:

Enabling sandboxing makes Claude Code secure.

Better:

Enabling sandboxing significantly reduces risk for Bash operations, especially when combined with tight filesystem and network restrictions.

The second sentence is more precise, more credible, and more useful.


14) A short “how to think about it” summary

If your reader is confused, give them this checklist:

If you want Claude to avoid secrets

Use permission denies.

Example:

"deny": ["Read(./.env)", "Read(~/.ssh/**)"]

If you want Bash to be contained

Use sandboxing.

Example:

If you want smarter runtime checks

Use hooks.

Example:

If you want company-wide enforcement

Use managed settings.

Example:

That kind of summary helps readers connect the tool to the job.


Final takeaway

If you are locking down Claude Code, think in layers.

Do not rely on prompts when you need policy. Do not rely on model instructions when you need enforcement. Do not rely on string matching when you need isolation. And do not rely on local preference when you need enterprise control.

Use:

That is the clearest path to a safer Claude Code deployment.


Related Posts

Questions or feedback? Reach out on LinkedIn