A small, reusable Claude Code hardening tool that can be dropped into any
project with a single command — claude-sec . (see
Installing into a project below). It
does four things:
-
Ships a defensive
.claude/settings.jsonthat enables Claude Code's sandbox, blocks network access by default, and deniesWebFetch/WebSearch. -
Adds a
PreToolUsehook that constrains which paths Claude Code's file tools (Read,Write,Edit,MultiEdit,NotebookEdit) are allowed to touch, via two simple text policy files. -
Lets you opt into the official
security-guidance@claude-plugins-officialplugin (viaclaude-sec configure) so Claude Code automatically reviews code changes for security issues as they are made. -
Ships two skills:
claude-sec-fill-deny-paths— scans the project for credential- and secret-shaped files and writes them todeny-workspace-paths.txt. One-shot; the moment it writes the first rule, the file-access hook seals.claude/.claude-sec-generate-security-guidance— drafts a project-tailoredclaude-security-guidance.md(loaded by the security-guidance plugin) by reading the project's own docs and running a stack / industry interview when needed.
See First-time setup below for the recommended order.
The hook implementation, the policy file format, and the manual test
recipes are documented in
.claude/hardening/README.md. This file
documents the project layout and every parameter used in
.claude/settings.json.
The repository ships a small installer at the root, claude-sec.
Installation steps:
git clone https://github.com/ComposableSecurity/claude-sec ~/.claude-sec # put the repo wherever you like
cd ~/.claude-sec
./claude-sec self-install # one-time: symlink into PATH
cd ~/some-project
claude-sec . # drop the security floor into ./.claude and optionally configure additional layers
# then open Claude Code in this project and run:
# /claude-sec-fill-deny-paths (scans + populates the deny list)
# /claude-sec-generate-security-guidance (only if you enabled the plugin)
# after the deny list is filled, back in your terminal:
claude-sec update-paths # mirror the deny list into sandbox.filesystemTo upgrade later:
claude-sec update # git pull inside the cloned repo
cd ~/some-project && claude-sec . # re-install the security floor and re-confirm optional layers (idempotent)| Subcommand | What it does |
|---|---|
claude-sec . |
Drops a minimal security floor into ./.claude (sandbox, permissions, file-access hook, SessionStart nudge). Optional layers are not included here. |
claude-sec configure |
Interactively walks each optional layer (security-guidance plugin, Bash review prompt, MCP audit prompt) read from ./.claude/hardening/features/ and merges the chosen ones into ./.claude/settings.json. Idempotent — safe to re-run. Requires python3. |
claude-sec update-paths |
Mirrors every active rule from ./.claude/hardening/deny-workspace-paths.txt into sandbox.filesystem.denyRead and sandbox.filesystem.denyWrite in ./.claude/settings.json. Set-style merge: adds missing rules, never removes existing entries (so user-added external paths like ~/.ssh/ survive re-runs). Idempotent. Requires python3. |
claude-sec update |
git pull --ff-only inside the cloned repo. |
claude-sec self-install |
Symlinks the script into $CLAUDE_SEC_BIN_DIR/claude-sec so it works from any directory. Does not clone or move the repo — the symlink resolves back through to it. |
Defaults (overridable via env vars):
CLAUDE_SEC_BIN_DIR—~/.local/bin.
The hardening setup targets macOS and Linux. Windows hosts are not supported by this version.
First-time setup is split between the command line (where you pick which optional security layers to enable) and two skills in Claude Code (where the agent does the project-specific work that needs Claude's reasoning).
Run this in a terminal at the project root. It walks each optional layer and asks y/n:
- the
security-guidance@claude-plugins-officialplugin - the Bash review prompt (
hooks.PreToolUse[Bash]) - the MCP audit prompt (
hooks.PreToolUse[mcp__.*])
Whatever you say "yes" to is merged into .claude/settings.json.
Anything you decline is simply left out. The merge is idempotent —
re-running with the same answers is a no-op, and re-running with
different answers upserts hook entries by matcher (no duplicates).
Open Claude Code in the project and run the
/claude-sec-fill-deny-paths skill. It will:
- Refuse with a clear message if
.claude/hardening/deny-workspace-paths.txtalready has any active rules. (Re-running requires clearing the file from outside Claude Code.) - Walk the workspace for credential- and secret-shaped files
(
.env,.env.*,secrets/,*.pem, cloud-creds JSON, SSH/PGP keys, DB dumps, anything in.gitignorethat looks credential-shaped, …) and present a candidate list. - Ask you to keep / remove / add entries.
- Write the confirmed list to
.claude/hardening/deny-workspace-paths.txtin a single edit. - Tell you to run
claude-sec update-pathsto mirror those paths intosandbox.filesystem.denyRead/denyWriteand to add external sensitive paths (~/.ssh/,~/.aws/credentials, registry tokens, …) todenyRead.
The skill is one-shot by design: the moment it writes the
first rule, the file-access hook locks
deny-workspace-paths.txt (and the rest of .claude/) from
Claude Code's file tools. To re-run, clear every active rule from
.claude/hardening/deny-workspace-paths.txt from outside Claude
Code.
Back in your terminal, run:
claude-sec update-pathsThis mirrors every active rule from
.claude/hardening/deny-workspace-paths.txt into both
sandbox.filesystem.denyRead and sandbox.filesystem.denyWrite
in .claude/settings.json. The result: Bash and any other
sandboxed subprocess sees the same path restrictions that
Claude Code's file tools do.
The merge is set-style — it only adds rules that aren't
already present and never removes anything. That means it's
safe to re-run after editing deny-workspace-paths.txt (e.g.
if the file was cleared and refilled), and it's also safe to
hand-edit sandbox.filesystem.denyRead / denyWrite to add
external sensitive paths like ~/.ssh/ or ~/.aws/credentials
— those entries survive every subsequent update-paths run.
Only relevant if you enabled the security-guidance plugin in
Step 1. Open Claude Code and run
/claude-sec-generate-security-guidance. It will:
- Refuse with a clear message if the plugin isn't enabled in
.claude/settings.json, and tell you to runclaude-sec configurefirst. - Read the project's own docs (README,
SECURITY.md,CONTRIBUTING.md,docs/,architecture/, dependency manifests for stack identification, and optionally a docs / website URL you paste in). - Run an industry / stack interview (Web2 SaaS, Fintech, Web3 / DeFi, Crypto libs / wallets, AI / agentic, Infra / DevOps, Healthcare, Mobile, Embedded / IoT, …) when the repo doesn't provide enough context.
- Draft a tailored
claude-security-guidance.mdat the project root: "What this project is", "Stack", "Threat model" (adversaries / goals / protected assets), "Trust boundaries", and a "Review checklist" pulled from a matching industry seed. - Show you the draft and apply your edits before saving.
The plugin loads the saved file as additional context for every model-backed review.
This section documents every parameter used in .claude/settings.json.
- Value:
true - Controls: Enables Claude Code's built-in sandbox for
Bashand subprocess execution. - Why: Default-on sandboxing is the foundation of this hardening
setup. Combined with
sandbox.allowUnsandboxedCommands: falseandsandbox.failIfUnavailable: true, it forces all shell-level work through the sandbox or stops the session.
- Value:
true - Controls: Whether Claude Code should refuse to run when the sandbox is requested but unavailable on the host (e.g. unsupported OS, missing kernel features).
- Why: Default fail-closed. If the sandbox cannot start, the hardening setup loses its foundation; aborting is safer than silently degrading to unsandboxed execution.
- Value:
false - Controls: Whether
Bashcalls auto-allow themselves when they run inside the sandbox. - Why: Default off. The sandbox restricts what a Bash call can do, but it does not decide whether the call itself is desirable. Requiring an explicit per-call permission prompt keeps a human in the loop for every shell action.
- Value:
[] - Controls: Commands that should bypass the sandbox even when it is enabled.
- Why: Empty by default. Adding commands here weakens the security posture; do so only with a clear reason.
- Value:
false - Controls: Whether Claude Code is allowed to run commands outside the sandbox.
- Why: Prevents falling back to unsandboxed execution. Without
this,
sandbox.enabled: truecan be effectively bypassed.
- Value:
false - Controls: Allows a weaker fallback sandbox when running inside an already-sandboxed environment (e.g. a container or nested VM).
- Why: Default off. Opting into a weaker sandbox is precisely what we are trying to prevent; the outer environment may already be weaker than expected.
- Value:
false - Controls: Permits a weaker network-isolation mode on hosts where the full mode is unavailable.
- Why: Default off. Combined with
failIfUnavailable: true, the hook either gets the strong network isolation it expects or refuses to run.
- Default values:
["."]/[] - Controls: Read scope visible to sandboxed processes (Bash and
any other subprocess launched through the sandbox).
allowReadis the read allowlist;denyReadcarves holes out of it. EmptydenyReadships in the template; the futureclaude-sec update-pathssubcommand populates it with both the workspace deny list (mirrored fromdeny-workspace-paths.txt) and external sensitive paths the user picks. - Why: Pinning
allowReadto the workspace (.) keeps the sandbox from accidentally reading host secrets. ExtendingdenyReadafterwards layers in paths the project itself ships (workspace secrets) and host-level paths the user does not want the sandbox to touch (~/.ssh/,~/.aws/credentials, etc.). This is the sandbox-layer equivalent of the file-access hook'sdeny-workspace-paths.txt; they protect different channels and are both needed.
- Default values:
["."]/[] - Controls: Write scope visible to sandboxed processes.
allowWriteis the write allowlist;denyWritecarves holes out of it. EmptydenyWriteships in the template;claude-sec update-pathsextends it with the workspace deny paths (mirrored fromdeny-workspace-paths.txt) and leaves external sensitive paths out by default. - Why: Same rationale as the read-side keys, but for writes.
External sensitive paths are deliberately not added to
denyWriteby default because that would block legitimate shell operations (e.g.ssh-keygenwriting to~/.ssh/).
- Value:
[] - Controls: Allowlist of network destinations the sandbox may reach.
- Why: Empty by default. Combined with
deniedDomains: ["*"], this denies all outbound network access from sandboxed commands. Add specific entries here if a project genuinely needs them.
- Value:
["*"] - Controls: Denylist of network destinations.
"*"denies all. - Why: Default deny everything; opt back in narrowly through
allowedDomains.
- Value:
[] - Controls: Allowlist of Unix domain sockets reachable from the sandbox.
- Why: Deny by default. Add only the sockets a project needs (e.g. a local Docker socket).
- Value:
false - Controls: Whether to expose every Unix domain socket to sandboxed processes.
- Why: Default off. Prefer the narrow allowlist in
allowUnixSockets.
- Value:
false - Controls: Whether sandboxed processes may bind local ports.
- Why: Default deny. Prevents the agent from silently spinning up listening services.
- Value:
[] - Controls: macOS-specific Mach service names the sandbox may look up.
- Why: Empty by default. Only relevant on macOS; add names only if a tool needs to talk to a specific system service. Ignored on Linux.
- Value:
"disable" - Controls: Disables Claude Code's "bypass permissions" mode (the
one normally reached via
--dangerously-skip-permissionsor the in-app permission switcher), so it cannot be selected for this project. - Why: Bypass mode skips the entire
permissionsblock — including theWebFetch/WebSearchdenies and the file-access hook's effective enforcement. Disabling it locks in the hardening defaults so a user cannot turn the safety rails off with a single keystroke. This is a project-scoped lockout; it should be set in the project.claude/settings.jsonthat ships with the hardening setup.
- Value:
"default" - Controls: The base permission mode Claude Code starts in.
- Why: Explicitly avoids
bypassPermissions. Using the default mode keeps the per-tool allow / ask / deny rules in force.
- Value:
[] - Controls: Tools / patterns Claude Code may use without asking.
- Why: Empty by default. Add narrow entries only when you have decided a tool is always safe in this project.
- Value:
[] - Controls: Tools / patterns Claude Code must prompt the user about.
- Why: Reserved for future hardening; left empty initially.
- Value:
["WebFetch", "WebSearch"] - Controls: Tools / patterns Claude Code must refuse.
- Why:
WebFetchandWebSearchreach out to the network from the agent itself, bypassing the Bash sandbox. They are denied here as part of the defensive default.
- Value:
{ "security-guidance@claude-plugins-official": true } - Controls: Plugins from configured Claude Code marketplaces that should be enabled for this project.
- Why:
security-guidance@claude-plugins-officialis Anthropic's official security-guidance plugin. With it enabled, Claude Code automatically reviews code changes for security issues during normal edits (the plugin contributes the review hooks/skills; this entry just turns it on). It complements the local hardening — the.claude/hardening/hook stops the agent from touching unsafe paths, while this plugin reviews the content of changes the agent makes inside the allowed scope.
-
Matcher:
"startup|resume" -
Hook:
[ { "type": "command", "command": "bash .claude/hardening/check-session-start.sh" } ] -
Controls: Runs once at session start. Runs two independent checks and emits banner(s) for whichever fail; silent when both pass:
- Deny-list populated? If
.claude/hardening/deny-workspace-paths.txthas no active rules yet, asks Claude to greet the user and recommend/claude-sec-fill-deny-paths. - Security-guidance file present (when the plugin is on)?
If the
security-guidance@claude-plugins-officialplugin is enabled in.claude/settings.jsonbutclaude-security-guidance.mddoes not exist at the project root, asks Claude to recommend/claude-sec-generate-security-guidance.
Both banners are concatenated into a single
additionalContextpayload when both checks fire, with the instruction to ask about the deny-paths skill first (recommended) and remind the user that the guidance-generation skill is available afterwards. - Deny-list populated? If
- Matcher:
"Read|Write|Edit|MultiEdit|NotebookEdit" - Hook:
[ { "type": "command", "command": "bash .claude/hardening/check-file-access.sh || exit 2" } ] - Controls: Intercepts every built-in file-tool call and
decides allow or deny against the policy: the workspace deny
list (
deny-workspace-paths.txt), the read-only allow list (allow-paths.txt), and an implicit deny on.claude/once setup is sealed. See_claude/hardening/README.mdfor the full policy semantics.
- Matcher:
"mcp__.*" - Hook type:
"prompt" - Controls: Before any MCP tool call, asks Claude to audit the
tool — identify it, classify it as safe or dangerous using the
/security-reviewskill, refuse and surface a prompt when dangerous, or remember an approval and proceed when safe. The decision is made model-side, not by an external script. - Audit criteria for "dangerous": the tool can read/write/move files outside the workspace, reach blocked network destinations, execute arbitrary code or shell commands, or access secrets, credentials, tokens, or environment variables. Approved tools are remembered per session so the same audit isn't repeated.
- ⚠ Warning — this is AI guarding AI: the audit is performed by Claude reviewing Claude's own intended action. It is non-deterministic and cannot be relied on as a 100% effective control. Use it as defense-in-depth alongside the deterministic file-access hook and sandbox, not as a substitute for them.
- Matcher:
"Bash" - Hook type:
"prompt" - Controls: Before every Bash command, asks Claude to inspect
the command line for paths that the file-access policy would
block (outside-workspace or deny-listed) — including indirect
references like redirections, pipelines, and tools that consume
paths as arguments (
cat,cp,rm,tar,sed -i, etc.). If the command touches a sensitive path, Claude surfaces a permission prompt naming the path and the operation; otherwise it proceeds. - ⚠ Warning — this is AI guarding AI: the review is performed by Claude reviewing Claude's own intended action. It is non-deterministic and cannot be relied on as a 100% effective control. Use it as defense-in-depth alongside the deterministic file-access hook and sandbox, not as a substitute for them.
The path-policy semantics, the contents of allow-paths.txt and
deny-workspace-paths.txt, and the manual test recipes live in
.claude/hardening/README.md. Look
there when changing hook behavior or policy files.
- When you change
.claude/settings.json, update the Settings reference section above in the same change. - When you change hook scripts or policy files, update
.claude/hardening/README.md. - See
CLAUDE.mdfor the rules Claude Code itself should follow when modifying any of these files.
