DEV Community

Umang Kumar
Umang Kumar

Posted on

Claude Code Security Best Practices: A Practical Hardening Guide

Claude Code can read your repository, edit files, run shell commands and call MCP servers. That is exactly why it is useful, and exactly why it deserves the same care you give a CI runner with production credentials. This guide collects Claude Code security best practices that hold up in day-to-day work: what to configure, why each setting matters, and where each layer stops protecting you.

The short version: assume the agent will eventually read something hostile (a README, an issue, a web page, a tool result), and make sure that when it does, the worst thing it can do is small.

The threat model in one paragraph

Most real risk comes from three things meeting in one session: untrusted content the model reads, sensitive material it can reach (.env, ~/.aws, SSH keys, tokens in MCP configs), and a channel to act (shell, network, git push, an MCP tool that writes somewhere). Prompt injection is the usual trigger, but an honest mistake — a cleanup command with an empty variable, a confident git reset --hard — does the same damage. Every practice below removes one of those three ingredients or puts a checkpoint between them.

1. Write explicit permission rules instead of clicking through prompts

Claude Code reads permission rules from JSON settings files. Rules come in three kinds — allow, ask, and deny — and each names a tool and what it may do. A sensible starting point in ~/.claude/settings.json or a project's .claude/settings.json:

{
  "permissions": {
    "allow": [
      "Bash(npm run lint)",
      "Bash(npm run test *)"
    ],
    "ask": [
      "Bash(git push *)"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Bash(git push --force *)",
      "Bash(rm -rf *)"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

A few details from the settings documentation are worth knowing:

  • Lists merge across scopes. User, project, local and managed permissions.allow lists combine; one file cannot silently remove another file's entries.
  • deny and ask in a committed project file apply immediately, while project allow rules wait until each person trusts the folder. That ordering is the safe one.
  • bypassPermissions and auto default modes are ignored when set from project or local settings. A cloned repository cannot quietly turn off your prompts.

Treat command-pattern rules as tripwires, not walls. Bash(rm -rf *) does not match rm -fr, rm -r -f, or find . -delete. Pattern rules are good at catching the common spelling; they are not a complete description of "destructive".

2. Deny secrets on every route, not just one

Read(./.env) stops the Read tool. It says nothing on its own about cat .env through Bash, a Grep across the repo, or an MCP filesystem server rooted at your home directory. After you add a deny rule, test the other routes yourself: ask the agent to cat, grep, and less the same file and confirm each is refused.

Better still, keep production secrets off the developer machine entirely. Inject them at runtime from a secret manager for the commands that need them, so there is nothing in the working tree for an agent to read.

3. Review what "Yes, and don't ask again" has accumulated

When you approve a Bash command with "Yes, and don't ask again", Claude Code saves an allow rule to .claude/settings.local.json. Those approvals pile up quietly. Open that file every few weeks and delete anything broader than you would grant on purpose — Bash(python *) or Bash(curl *) grants far more than the one command you meant to approve.

4. Share the baseline; enforce it centrally for teams

Commit .claude/settings.json so every clone gets the same deny list. For organisations, managed settings sit above everything else and cannot be overridden by user or project files. Put your non-negotiables (secret paths, force-push, production deploy commands) there, and let individuals add convenience allow rules locally.

5. Treat every MCP server as third-party code with your credentials

MCP server definitions live in Claude Code's own config. Each one is a process that runs with your user's permissions and often holds a token. Practical rules:

  • Pin versions instead of npx -y some-server@latest.
  • Scope filesystem servers to the project directory, never / or ~.
  • Keep tokens out of inline env blocks where possible; anything inline is readable by anything that can read the config file.
  • Remove servers you are not using this week.

6. Use a PreToolUse hook for rules you want to test

Hooks let you run your own program before a tool executes. For PreToolUse, Claude Code writes a JSON payload (tool name and input) to the hook's stdin and reads a JSON decision back. Returning permissionDecision: "deny" with a reason blocks the call and shows the reason to the model, which usually lets it re-plan instead of retrying.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit|WebFetch",
        "hooks": [{ "type": "command", "command": "node /path/to/your-hook.mjs" }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The advantage over static patterns is that a hook can normalise the command, check paths after resolving them, look at session history, and be unit-tested in CI like any other code.

7. Isolate untrusted work

For repositories you did not write, run Claude Code in a container or VM with no host credentials mounted and limited network egress. Claude Code also documents a Bash sandbox option; use it. Isolation is the layer that still holds when every rule above was written slightly wrong.

8. Keep a record of what the agent decided and did

When something goes wrong, "what did the agent run, and what allowed it?" should take a minute to answer, not an afternoon of shell history archaeology. Log decisions with the rule that produced them, and keep that log somewhere the agent cannot edit.

Where Cirvix fits

Cirvix AgentControl is an open-source (Apache-2.0) policy layer that covers two of the layers above with one policy file. It evaluates governed tool calls before they execute and returns permit, hold (wait for a named approver) or deny, with deny as the default.

For Claude Code there are two paths, and they are complementary:

  • MCP gateway. Claude Code launches cirvix gateway as its only MCP entry; Cirvix launches your real servers and evaluates every routed tools/call first.
  • PreToolUse hook. integrations/claude-code/hook.mjs in the repo sends built-in tool calls (Bash, Write, Edit, WebFetch) to a local cirvix runtime over a socket. By default the hook fails open with a stderr warning if the runtime is not running; set CIRVIX_HOOK_FAIL=closed in CI or anywhere you need fail-closed behaviour.

Try a decision in under a minute, without executing anything:

npx --yes @cirvix_ai/agent-control check --action fs.read --resource .env.production
# DENY  fs.read …/.env.production   rule deny-dotenv-read   (exit 1)
Enter fullscreen mode Exit fullscreen mode

Be clear about the boundary: Cirvix governs calls routed through the gateway, the hook, or its SDK wrappers. It does not inspect prompts, does not prevent prompt injection, and does not see a terminal you type into yourself. It narrows what an injected or mistaken agent can do, which is the part you can actually control.

Checklist

  • [ ] Explicit deny rules for secrets, force-push and destructive commands
  • [ ] Tested every route to secrets (Read, Bash, Grep, MCP)
  • [ ] Reviewed .claude/settings.local.json approvals
  • [ ] Shared project settings committed; org rules in managed settings
  • [ ] MCP servers pinned, scoped, and pruned
  • [ ] A PreToolUse hook (or gateway) for testable policy
  • [ ] Untrusted repos run in isolation
  • [ ] Decisions logged somewhere the agent cannot edit

Cirvix AgentControl is open source: https://github.com/CIRVIX/agent-control
More guides and docs: https://cirvix.com

Top comments (0)