Claude Code Hardening Guide
Harden Claude Code managed policies, permissions, plugins, isolation, remote execution, cloud-provider routing and CI credentials.
Overview
Claude Code is Anthropic’s agentic coding tool, running in developer terminals, IDEs, and CI/CD pipelines with the ability to read code, execute commands, and modify files. That capability profile makes it a first-class security surface: a misconfigured deployment can exfiltrate source code, execute injected instructions from untrusted content, or grant third-party MCP servers standing access to internal systems.
This is a product guide within the Anthropic platform. Organization-wide controls (SSO, roles, admin API keys, integration governance) live in the Anthropic Common Controls hub; API/Console platform controls (workspaces, API keys, spend limits) live in the Claude API & Console guide.
Research and validation boundary
This refresh used the source-first candidate ledger and public vendor references, including the published Cowork/unified baseline. It did not access a live tenant, change settings or create credentials. Source verification and mocked/native-interface tests do not earn live-validation status. The exact applicable Security Center checklist must be reconciled with an Owner’s current report; the effective-settings schema is a separate evidence surface.
Intended Audience
- Security engineers governing AI coding tools
- Platform/IT teams deploying Claude Code via MDM at fleet scale
- DevSecOps teams running Claude Code in CI/CD
- Incident responders covering agentic-tool compromise
How to Use This Guide
- L1 (Crawl): Essential controls for all organizations
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest controls for regulated industries
Scope
This guide covers Claude Code deployment policy (managed settings), permission and tool restriction, MCP server access control, hooks/plugins lockdown, bash sandbox and external sandbox isolation, prompt-injection and rules-file-attack defense, CI/CD pipeline hardening, developer metrics, Cowork collaborative-session governance, and incident response. Organization identity and API platform controls are covered by the sibling Anthropic guides.
Table of Contents
- Policy & Deployment
- Extensions & Supply Chain
- Execution Isolation
- Threat Defense
- Monitoring, Collaboration & Incident Response
1. Policy & Deployment
1.1 Deploy Managed Settings via MDM
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| NIST 800-53 | CM-6, CM-7 |
| SOC 2 | CC6.1, CC8.1 |
Description
Deploy organization-wide Claude Code security policies using one of four managed settings delivery mechanisms. Managed settings cannot be overridden by user or project settings. The mechanisms are: (1) Server-managed settings via the Claude.ai admin console (Team v2.1.38+ / Enterprise v2.1.30+), requiring no MDM; (2) MDM/OS-level policies via macOS managed preferences (com.anthropic.claudecode domain in Jamf/Kandji) or the Windows registry (HKLM\SOFTWARE\Policies\ClaudeCode via GPO/Intune); (3) File-based managed-settings.json deployed to system paths, plus an optional managed-settings.d/ drop-in directory whose *.json fragments merge onto the base file; (4) the HKCU registry (HKCU\SOFTWARE\Policies\ClaudeCode, Windows and WSL), a per-user fallback that Claude Code reads only when no other managed source delivers a policy key. Most managed keys use the highest-ranked source carrying a policy key by default; managedSourcesBehavior: "merge" can combine sources. Current versions have security-sensitive cross-source exceptions for lock keys, env, provider restrictions and sign-in keys. Review the current precedence rules instead of assuming the whole policy is first-wins.
Rationale
Why This Matters:
- Without managed settings, individual developers can use
--dangerously-skip-permissionsto bypass all safety checks - User-defined hooks and MCP servers can introduce supply chain risks
- Managed settings enforce a security baseline that developers cannot weaken
- Server-managed settings are fetched at startup and polled hourly with offline caching
Attack Prevented: Permission bypass, unauthorized tool execution, malicious hook injection
Prerequisites
- MDM solution deployed to developer machines (Jamf, Intune, Kandji), OR Claude Team/Enterprise plan for server-managed settings
- Security team consensus on default permission mode and deny rules
- Inventory of approved MCP servers and tools
ClickOps Implementation
Option A: Server-Managed Settings (No MDM Required)
- Navigate to: claude.ai → Organization settings → Claude Code → Managed settings (claude.ai/admin-settings/claude-code; Owner or Primary Owner)
- Add JSON configuration with required security settings
- Confirm current provider/authentication eligibility and the active source on each client. A first launch can briefly run before the remote fetch finishes or continue after a failed fetch; cached settings also have withheld values. Where remote policy is mandatory, evaluate managed
forceRemoteSettingsRefresh: truewith the current documented client/version and failure behavior before rollout. Keep an endpoint baseline for unsupported providers and recovery. - In v2.1.223+, administrative
envsources merge per key, with telemetry/credential-routing exceptions; older versions differ. Credential/profile selectors such asANTHROPIC_PROFILEare startup-only and must be delivered through endpoint policy, not a late server update. For first-party sign-in on clients older than v2.1.299, set org/login keys in both server and endpoint policy as documented. - Deliver
managed-mcp.jsonthrough its separate system path, or use the documented policy allow/deny keys. Server-providedmanagedMcpServersrequires v2.1.259+ and does not have the file’s exclusive-control behavior. Do not copy OS-onlypolicyHelperor gateway sign-in keys into cloud policy.
Option B: MDM Deployment
Step 1: Create managed-settings.json
- Create the JSON configuration file with your organization’s security policy
- Include at minimum:
permissions.disableBypassPermissionsModeset to"disable",permissions.denyrules, andpermissions.defaultMode. The bypass key must sit inside thepermissionsobject — Claude Code accepts a root-level copy in the file but never applies it
Step 2: Deploy via MDM
Deploy to the correct OS-specific path:
| OS | File Path | MDM/Policy Path |
|---|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-settings.json |
com.anthropic.claudecode preferences domain (Jamf/Kandji profile) |
| Linux / WSL | /etc/claude-code/managed-settings.json |
N/A |
| Windows | C:\Program Files\ClaudeCode\managed-settings.json |
HKLM\SOFTWARE\Policies\ClaudeCode → Settings (REG_SZ with JSON) |
For modular policies, create a managed-settings.d/ directory alongside the base file. Use numeric prefixes to control merge order (e.g., 10-telemetry.json, 20-security.json). Files are sorted alphabetically, deep-merged onto the base — arrays are concatenated and de-duplicated, objects are deep-merged, and later files override earlier ones for scalar values.
Step 3: Verify Deployment
- On a test machine, run
/statusinside Claude Code and read the Setting sources line: it must showEnterprise managed settingswith the selected source in parentheses —(file),(drop-ins), or(file + drop-ins)for this option,(plist)or(HKLM)for MDM,(remote)for server-managed settings. A Skipped sources line names any managed source that was present but overridden by a higher-ranked one - Run
claude doctorto list any entries Claude Code dropped as invalid — an invalid top-level key is dropped while the rest of the policy keeps enforcing - Attempt to use
--dangerously-skip-permissions— should be blocked ifpermissions.disableBypassPermissionsModeis"disable"
Time to Complete: ~30 minutes (policy creation) + MDM deployment time
Code Implementation
Code Pack: Config
// Anthropic official example: settings-lax.json (L1 Crawl)
// Source: github.com/anthropics/claude-code/blob/main/examples/settings/settings-lax.json
// Prevents --dangerously-skip-permissions and blocks plugin marketplaces.
{
"permissions": {
"disableBypassPermissionsMode": "disable"
},
"strictKnownMarketplaces": []
}
// Anthropic official example: settings-strict.json (L2 Walk)
// Source: github.com/anthropics/claude-code/blob/main/examples/settings/settings-strict.json
// Blocks bypass, enforces managed-only permissions and hooks,
// denies web access, requires Bash approval, locks sandbox settings.
{
"permissions": {
"disableBypassPermissionsMode": "disable",
"ask": [
"Bash"
],
"deny": [
"WebSearch",
"WebFetch"
]
},
"allowManagedPermissionRulesOnly": true,
"allowManagedHooksOnly": true,
"strictKnownMarketplaces": [],
"sandbox": {
"autoAllowBashIfSandboxed": false,
"excludedCommands": [],
"network": {
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false,
"allowedDomains": []
},
"enableWeakerNestedSandbox": false
}
}
// Anthropic official example: settings-bash-sandbox.json (L3 Sandbox)
// Source: github.com/anthropics/claude-code/blob/main/examples/settings/settings-bash-sandbox.json
// Enables OS-level bash sandboxing with no escape hatch.
// Platform support: macOS (Seatbelt), Linux/WSL2 (bubblewrap).
{
"allowManagedPermissionRulesOnly": true,
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": false,
"allowUnsandboxedCommands": false,
"excludedCommands": [],
"network": {
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false,
"allowedDomains": []
},
"enableWeakerNestedSandbox": false
}
}
// Comprehensive managed-settings.json — combines all security controls.
// Reference: code.claude.com/docs/en/settings
// Extends the official examples with practical deny rules, model
// restrictions, org login enforcement, and MCP server controls.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"disableBypassPermissionsMode": "disable",
"deny": [
"Bash(curl *)",
"Bash(wget *)",
"Bash(rm -rf *)",
"Bash(ssh *)",
"Bash(scp *)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(./credentials/**)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"WebSearch",
"WebFetch"
],
"ask": [
"Bash"
],
"allow": [
"Bash(npm run *)",
"Bash(npm test)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)"
]
},
"allowManagedPermissionRulesOnly": true,
"allowManagedHooksOnly": true,
"strictKnownMarketplaces": [],
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
},
"model": "claude-sonnet-4-6",
"availableModels": ["sonnet", "haiku"],
"forceLoginMethod": "claudeai",
"forceLoginOrgUUID": "REPLACE-WITH-YOUR-ORG-UUID",
"cleanupPeriodDays": 7,
"allowedMcpServers": [
{"serverName": "github"},
{"serverName": "memory"}
],
"deniedMcpServers": [
{"serverName": "filesystem"},
{"serverName": "shell"},
{"serverName": "puppeteer"}
],
"allowManagedMcpServersOnly": true,
"channelsEnabled": false,
"disableAutoMode": "disable",
"disableDeepLinkRegistration": "disable",
"useAutoModeDuringPlan": false,
"autoMode": {
"environment": [
"Organization: REPLACE-WITH-YOUR-ORG. Primary use: software development",
"Source control: REPLACE-WITH-YOUR-SCM-HOST and all repos under it",
"Trusted internal domains: REPLACE-WITH-YOUR-INTERNAL-DOMAINS"
]
},
"blockedMarketplaces": [],
"pluginTrustMessage": "Only IT-approved plugins are permitted.",
"allowedHttpHookUrls": ["https://hooks.example.com/*"],
"httpHookAllowedEnvVars": ["HOOK_AUTH_TOKEN"],
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"autoAllowBashIfSandboxed": false,
"allowUnsandboxedCommands": false,
"excludedCommands": [],
"filesystem": {
"denyRead": ["~/.aws/credentials", "~/.ssh/id_*"],
"denyWrite": ["/etc", "/usr/local/bin"],
"allowWrite": ["/tmp/build"]
},
"network": {
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false,
"allowedDomains": [
"github.com",
"*.npmjs.org",
"registry.yarnpkg.com"
],
"allowManagedDomainsOnly": true
},
"enableWeakerNestedSandbox": false
}
}
// Security note: Custom script settings that execute arbitrary commands.
// These settings run shell scripts during Claude Code operation.
// Audit any scripts referenced by these settings for injection risks.
//
// apiKeyHelper — runs a script to generate auth headers for model requests
// otelHeadersHelper — runs a script to generate OpenTelemetry headers
// awsAuthRefresh — runs a script that modifies the .aws directory
// awsCredentialExport — runs a script that outputs AWS credentials as JSON
//
// Recommendation: In managed settings, either omit these (use standard auth)
// or restrict to vetted scripts at known paths. Monitor script contents
// for changes via file integrity monitoring.
// DANGER: autoMode.allow and autoMode.soft_deny REPLACE the entire default
// rule lists when set. Setting soft_deny with a single entry discards ALL
// built-in block rules: force push, data exfiltration, curl|bash, production
// deploys, and every other default block rule become allowed.
//
// Safe approach:
// 1. Run: claude auto-mode defaults — to get the full built-in lists
// 2. Copy the entire list into your settings
// 3. Add or remove individual rules
// 4. Run: claude auto-mode config — to verify effective rules
// 5. Run: claude auto-mode critique — to get AI feedback on custom rules
//
// Only autoMode.environment is safe to set without side effects — it does
// not replace any default lists, only tells the classifier what is trusted.
//
// autoMode is NOT read from shared project settings (.claude/settings.json)
// to prevent checked-in repos from injecting their own allow rules.
// Optional managed fragment where fresh server policy is mandatory. Pilot
// documented version/auth/provider support and failure recovery before rollout.
// Availability loss can block startup; it is not appropriate for every deployment.
{
"forceRemoteSettingsRefresh": true
}
Code Pack: API Script
# Locate the file-based managed settings directory for this OS
STAGED=0
if [[ -n "${HTH_CLAUDE_MANAGED_DIR:-}" ]]; then
MANAGED_DIR="${HTH_CLAUDE_MANAGED_DIR}"; STAGED=1
else
case "$(uname -s)" in
Darwin) MANAGED_DIR="/Library/Application Support/ClaudeCode" ;;
Linux) MANAGED_DIR="/etc/claude-code" ;;
MINGW*|MSYS*|CYGWIN*) MANAGED_DIR="C:/Program Files/ClaudeCode" ;;
*)
fail "7.1 Unknown OS — cannot locate the managed settings directory"
summary; exit 1
;;
esac
fi
info "Checking file-based managed settings in: ${MANAGED_DIR}"
# Base file first, then drop-ins in alphabetical order (hidden files ignored)
SOURCES=()
if [[ -f "${MANAGED_DIR}/managed-settings.json" ]]; then
SOURCES+=("${MANAGED_DIR}/managed-settings.json")
fi
LC_COLLATE=C
for f in "${MANAGED_DIR}/managed-settings.d/"*.json; do
if [[ -f "${f}" ]]; then SOURCES+=("${f}"); fi
done
if [[ ${#SOURCES[@]} -eq 0 ]]; then
fail "7.1 No managed-settings.json or managed-settings.d/*.json found — file-based managed settings are not deployed"
summary; exit 1
fi
# Claude Code refuses to start when a managed file is not a JSON object
for f in "${SOURCES[@]}"; do
if jq -e 'type == "object"' "${f}" >/dev/null 2>&1; then
pass "7.1 ${f} is a valid JSON object"
else
fail "7.1 ${f} is not a JSON object — Claude Code refuses to start when it cannot parse a managed file"
fi
done
if [[ ${FAILED} -gt 0 ]]; then summary; exit 1; fi
# Merge the sources the way Claude Code does
MERGED=$(jq -s '
def m($a; $b):
if ($a|type) == "object" and ($b|type) == "object" then
reduce ($b|keys[]) as $k ($a; .[$k] = m($a[$k]; $b[$k]))
elif ($a|type) == "array" and ($b|type) == "array" then ($a + $b | unique)
else $b end;
reduce .[] as $x ({}; m(.; $x))' "${SOURCES[@]}")
merged() { printf '%s' "${MERGED}" | jq "$@"; }
# disableBypassPermissionsMode is read only under "permissions"; a root-level
# copy is accepted by the file but never applied
if [[ "$(merged -r 'has("disableBypassPermissionsMode")')" == "true" ]]; then
fail "7.1 disableBypassPermissionsMode is at the root — Claude Code reads it only as permissions.disableBypassPermissionsMode"
fi
BYPASS=$(merged -r '.permissions.disableBypassPermissionsMode // "not set"')
DENY_COUNT=$(merged '.permissions.deny // [] | length')
DEFAULT_MODE=$(merged -r '.permissions.defaultMode // "not set"')
MANAGED_PERMS_ONLY=$(merged -r 'if .allowManagedPermissionRulesOnly == null then "not set" else .allowManagedPermissionRulesOnly end')
info "Merged policy (${#SOURCES[@]} file(s)):"
info " permissions.disableBypassPermissionsMode: ${BYPASS}"
info " permissions.deny rules: ${DENY_COUNT}"
info " permissions.defaultMode: ${DEFAULT_MODE}"
info " allowManagedPermissionRulesOnly: ${MANAGED_PERMS_ONLY}"
if [[ "${BYPASS}" == "disable" ]]; then
pass "7.1 Bypass permissions mode is disabled"
else
fail "7.1 permissions.disableBypassPermissionsMode is not \"disable\" — --dangerously-skip-permissions still works"
fi
if [[ "${DENY_COUNT}" -gt 0 ]]; then
merged -r '.permissions.deny[]' | while IFS= read -r rule; do info " - ${rule}"; done
pass "7.1 Deny rules are configured (${DENY_COUNT} rules)"
else
fail "7.1 No permissions.deny rules in the managed policy"
fi
if [[ "${DEFAULT_MODE}" == "not set" ]]; then
warn "7.1 permissions.defaultMode is not set in the managed policy"
fi
if [[ "${MANAGED_PERMS_ONLY}" == "true" ]]; then
pass "7.1 Only managed permission rules are enforced"
else
warn "7.1 allowManagedPermissionRulesOnly is not enabled (see Control 7.2)"
fi
# Sources are first-wins by default: a higher-ranked source that carries any
# policy key is used and this file is skipped without a warning
if [[ ${STAGED} -eq 0 ]]; then
if [[ -f "/Library/Managed Preferences/com.anthropic.claudecode.plist" ]]; then
warn "7.1 An MDM profile (com.anthropic.claudecode) outranks this file — unless it sets managedSourcesBehavior: \"merge\", Claude Code skips the file"
fi
if command -v reg.exe >/dev/null 2>&1 && reg.exe query 'HKLM\SOFTWARE\Policies\ClaudeCode' >/dev/null 2>&1; then
warn "7.1 An HKLM policy (SOFTWARE\\Policies\\ClaudeCode) outranks this file — unless it sets managedSourcesBehavior: \"merge\", Claude Code skips the file"
fi
info "Server-managed settings (claude.ai) also outrank this file; run /status in Claude Code and read the Setting sources line to confirm which source is in force"
fi
Validation & Testing
- Run the local-file validation script against staged policy. It checks base/drop-in content only and cannot establish effective cloud/MDM policy, endpoint version or all cross-source exception semantics
- Verify
permissions.disableBypassPermissionsModeis set to"disable" - Attempt
--dangerously-skip-permissions— should be rejected - Verify deny rules block restricted operations
Expected result: All developer machines have managed settings deployed; bypass mode is disabled
Monitoring & Maintenance
Ongoing monitoring:
- MDM compliance dashboard confirms file is present on all enrolled devices
- Alert on devices missing managed-settings.json
Maintenance schedule:
- Monthly: Review and update deny rules as tooling changes
- Quarterly: Audit managed settings against security policy
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | Medium | Developers cannot bypass permission checks |
| System Performance | None | Settings loaded once at startup |
| Maintenance Burden | Low | MDM handles deployment; policy changes are centralized |
| Rollback Difficulty | Easy | Remove file from MDM profile |
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1, CC8.1 | Logical access security; change management |
| NIST 800-53 | CM-6, CM-7 | Configuration settings; least functionality |
| ISO 27001 | A.12.5.1 | Installation of software on operational systems |
1.2 Restrict Claude Code Permissions and Tools
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | AC-3, CM-7 |
| SOC 2 | CC6.1, CC6.3 |
Description
Configure granular permission rules in managed settings to control which tools Claude Code can use, which files it can access, and which commands it can execute. Use deny rules (which always take precedence) to block sensitive operations like reading .env files, executing curl commands, or accessing secrets directories.
Rationale
Why This Matters:
- File edits and shell execution normally require permissions; read-only operations may be preapproved, and bypass/auto modes and installed extensions materially change the boundary
- Without restrictions, a compromised or confused AI agent could exfiltrate secrets, modify production configs, or execute malicious commands
- Deny rules take precedence over allow rules for matching tool requests; Bash text matching is not a security boundary around an executable. Alternate paths, wrappers or other tools may not match the same text
allowManagedPermissionRulesOnly: trueensures users cannot add their own allow rules to weaken the policy
Attack Prevented: Secret exfiltration via AI agent, unauthorized file access, command injection
Prerequisites
- Managed settings deployment (Control 1.1)
- Inventory of sensitive file patterns and restricted commands
ClickOps Implementation
Step 1: Define Permission Policy
- Identify sensitive file patterns:
.env,.env.*,secrets/, credentials files - Identify restricted commands:
curl(data exfiltration),rm -rf(destruction), credential access - Define approved operations:
npm run *,git status,git diff, test runners
Step 2: Configure via Admin Console or MDM
- Add permission rules to managed settings:
- deny:
Read(./.env),Read(./.env.*),Read(./secrets/**),Bash(curl *),Bash(rm -rf *) - allow:
Bash(npm run *),Bash(git status),Bash(git diff *) - ask:
Bash(git push *),Bash(git commit *)
- deny:
- Set
allowManagedPermissionRulesOnly: trueto prevent user overrides - Set
permissions.disableBypassPermissionsMode: "disable"— inside thepermissionsobject, not at the root (see Control 1.1)
Step 3: Configure Network Sandbox (L3)
- Enable
sandbox.enabled: truefor OS-level isolation - Set
sandbox.network.allowedDomainsto restrict outbound network access - Restrict socket access as needed
Time to Complete: ~20 minutes
Code Implementation
Code Pack: Config
// Example permission deny/ask/allow rules for managed-settings.json
// Deny blocks access. Ask prompts the user. Allow auto-approves.
// Glob patterns: * matches files in one directory, ** matches recursively.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(./credentials/**)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(rm -rf *)",
"Bash(ssh *)",
"Bash(scp *)",
"Bash(nc *)",
"Bash(base64 *)",
"WebSearch",
"WebFetch"
],
"ask": [
"Bash",
"Bash(git push *)",
"Bash(git commit *)",
"Bash(docker *)",
"Bash(kubectl *)"
],
"allow": [
"Bash(npm run *)",
"Bash(npm test)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(ls *)",
"Bash(cat package.json)",
"Read",
"Edit"
]
}
}
// Agent() permission rules — control subagent spawning
// Reference: code.claude.com/docs/en/permissions
// Agent() rules follow the same deny/ask/allow evaluation as other tools.
// Specifier is the subagent_type name from the Agent tool definition.
// Use this to prevent Claude from spawning unrestricted background agents.
{
"permissions": {
"deny": [
"Agent(general-purpose)",
"Agent(BrowserAgent)"
],
"ask": [
"Agent"
],
"allow": [
"Agent(Explore)",
"Agent(Plan)"
]
}
}
// Security note: Permission deny rules have tool-specific scope.
// Read(./.env) only blocks the Read tool — it does NOT prevent:
// Bash(cat .env) — the Bash tool is a separate permission
// Bash(grep -r PASSWORD .env)
// Bash(base64 .env)
//
// To fully protect sensitive files, you MUST combine:
// 1. Read() deny rules for the Read tool
// 2. Bash() deny rules for common exfiltration commands (curl, cat, etc.)
// 3. Sandbox filesystem.denyRead for OS-level enforcement (strongest)
//
// Only sandbox denyRead provides true defense-in-depth because it operates
// at the OS level and cannot be bypassed by creative Bash commands.
//
// Also note: permissions.additionalDirectories grants Claude Code
// access to directories outside the project root. If set, ensure deny
// rules also cover those paths (e.g., Read(/data/secrets/**)).
// Enforce managed-only permission rules
// Reference: code.claude.com/docs/en/settings
// When allowManagedPermissionRulesOnly is true, user and project
// settings cannot define allow, ask, or deny rules — only rules
// from managed settings apply.
{
"permissions": {
"disableBypassPermissionsMode": "disable",
"defaultMode": "default"
},
"allowManagedPermissionRulesOnly": true,
"allowManagedHooksOnly": true,
"disableAllHooks": false
}
// OS-level bash sandbox configuration (L3 — Run)
// Reference: code.claude.com/docs/en/sandboxing
// Sandboxing provides filesystem and network isolation for Bash commands.
// Platform support:
// macOS: Seatbelt (built-in, no install needed)
// Linux/WSL2: bubblewrap (apt install bubblewrap socat)
// Windows: Not yet supported
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": false,
"allowUnsandboxedCommands": false,
"excludedCommands": [],
"network": {
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false,
"allowedDomains": [
"*.npmjs.org",
"registry.npmjs.org",
"github.com"
]
},
"enableWeakerNestedSandbox": false
}
}
Code Pack: API Script
# Validate permission configuration on this machine
if [[ -n "${HTH_CLAUDE_MANAGED_DIR:-}" ]]; then
MANAGED_DIR="${HTH_CLAUDE_MANAGED_DIR}"
else
case "$(uname -s)" in
Darwin) MANAGED_DIR="/Library/Application Support/ClaudeCode" ;;
Linux) MANAGED_DIR="/etc/claude-code" ;;
MINGW*|MSYS*|CYGWIN*) MANAGED_DIR="C:/Program Files/ClaudeCode" ;;
*) fail "7.2 Unknown OS — cannot locate the managed settings directory"; summary; exit 1 ;;
esac
fi
# Base file first, then drop-ins in alphabetical order (hidden files ignored)
SOURCES=()
if [[ -f "${MANAGED_DIR}/managed-settings.json" ]]; then
SOURCES+=("${MANAGED_DIR}/managed-settings.json")
fi
LC_COLLATE=C
for f in "${MANAGED_DIR}/managed-settings.d/"*.json; do
if [[ -f "${f}" ]]; then SOURCES+=("${f}"); fi
done
if [[ ${#SOURCES[@]} -eq 0 ]]; then
fail "7.2 No managed-settings.json or managed-settings.d/*.json in ${MANAGED_DIR} — permissions are not restricted by managed policy"
summary; exit 1
fi
MERGED=$(jq -s '
def m($a; $b):
if ($a|type) == "object" and ($b|type) == "object" then
reduce ($b|keys[]) as $k ($a; .[$k] = m($a[$k]; $b[$k]))
elif ($a|type) == "array" and ($b|type) == "array" then ($a + $b | unique)
else $b end;
reduce .[] as $x ({}; m(.; $x))' "${SOURCES[@]}") || {
fail "7.2 A managed settings file is not valid JSON"; summary; exit 1
}
merged() { printf '%s' "${MERGED}" | jq "$@"; }
DENY_COUNT=$(merged '.permissions.deny // [] | length')
ALLOW_COUNT=$(merged '.permissions.allow // [] | length')
ASK_COUNT=$(merged '.permissions.ask // [] | length')
info "Permission rules across ${#SOURCES[@]} file(s): deny=${DENY_COUNT}, allow=${ALLOW_COUNT}, ask=${ASK_COUNT}"
if [[ "${DENY_COUNT}" -gt 0 ]]; then
pass "7.2 Deny rules configured (${DENY_COUNT} rules)"
else
fail "7.2 No deny rules — sensitive files and commands are unrestricted"
fi
PERMS_ONLY=$(merged -r '.allowManagedPermissionRulesOnly // false')
if [[ "${PERMS_ONLY}" == "true" ]]; then
pass "7.2 Managed-only permission rules enforced"
else
fail "7.2 allowManagedPermissionRulesOnly is not enabled — users can add their own allow rules"
fi
# Step 2.3: bypass mode must be disabled inside the permissions object
if [[ "$(merged -r 'has("disableBypassPermissionsMode")')" == "true" ]]; then
fail "7.2 disableBypassPermissionsMode is at the root — Claude Code reads it only as permissions.disableBypassPermissionsMode"
fi
BYPASS=$(merged -r '.permissions.disableBypassPermissionsMode // "not set"')
if [[ "${BYPASS}" == "disable" ]]; then
pass "7.2 Bypass permissions mode is disabled"
else
fail "7.2 permissions.disableBypassPermissionsMode is not \"disable\""
fi
SANDBOX_ENABLED=$(merged -r '.sandbox.enabled // false')
if [[ "${SANDBOX_ENABLED}" == "true" ]]; then
pass "7.2 Bash sandbox is enabled"
# jq's // treats false as missing, so read the boolean explicitly (default: true)
UNSANDBOXED=$(merged -r 'if .sandbox.allowUnsandboxedCommands == null then true else .sandbox.allowUnsandboxedCommands end')
if [[ "${UNSANDBOXED}" == "false" ]]; then
pass "7.2 Unsandboxed command escape hatch is disabled"
else
warn "7.2 sandbox.allowUnsandboxedCommands is not false — users can bypass the sandbox"
fi
else
info "7.2 Bash sandbox is not enabled (optional L3 control)"
fi
Validation & Testing
- Attempt to read a
.envfile via Claude Code — should be denied - Attempt to run
curlvia Claude Code — should be denied - Run an approved command (e.g.,
npm run test) — should succeed - Verify user-added allow rules are ignored when
allowManagedPermissionRulesOnlyis true
Expected result: Sensitive files and commands are blocked; only approved operations succeed
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | Medium | Developers see denials for restricted operations |
| System Performance | None | Rule evaluation is instant |
| Maintenance Burden | Medium | Rules need updating as tooling evolves |
| Rollback Difficulty | Easy | Remove deny rules from managed settings |
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1, CC6.3 | Logical access security; role-based access |
| NIST 800-53 | AC-3, CM-7 | Access enforcement; least functionality |
| ISO 27001 | A.9.4.1 | Information access restriction |
1.3 Gate Remote Control, Cloud Sessions and Routines
Profile Level: L2 (Walk)
Description
Review the separate Code remote/cloud/routine capabilities before enabling work that persists beyond the terminal. Require trusted devices for approved Remote Control and constrain repositories, environments and credentials for cloud work.
Rationale
Why This Matters:
- Local Remote Control and Code on the web execute in different places and have different data/network boundaries.
- Member sign-out leaves cloud sessions/routines running, and trusted-device enforcement only covers newly started remote sessions.
Attack Prevented: Unauthorized remote steering and persistent cloud-task access after account containment.
ClickOps Implementation
- Review Organization settings → Claude Code capabilities for remote control, web/cloud sessions, routines, review/security features and permission bypass. Use groups/custom roles for intended populations.
- For approved Remote Control enable Capabilities → Remote sessions → Require trusted devices and restart existing remote sessions through normal change control. Review device enrollment and lost-device revocation separately.
- For local Code deployments that must prohibit Remote Control, deploy documented managed
disableRemoteControl: true; this is not a switch for every cloud product. - Review repositories, runtime environments, network policy, inherited secrets and schedule/event triggers for Code cloud work. Stop those sessions and routines explicitly during an incident rather than assuming sign-out did it.
Code Implementation
Code Pack: Config
{
"disableRemoteControl": true,
"channelsEnabled": false,
"allowedChannelPlugins": [],
"permissions": {
"disableBypassPermissionsMode": "disable"
}
}
Validation & Testing
- Read returned
claude_code_remote_control_enabled,claude_code_trusted_devices_required,claude_code_web_enabledandclaude_code_routines_enabledas separate evidence; missing rows remain unknown. - Verify denied remote access and allowed pilot use, cloud repository scope and routine termination in the actual supported product.
Expected result: The documented boundary is exercised and any unsupported surface or exception is recorded.
Operational Impact
Trusted Devices is beta and off by default; it affects remote steering, not ordinary terminal/API activity. ZDR/HIPAA availability is restricted.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls | 4.1, 6.8 | Secure configuration and role-based access |
| NIST 800-53 | AC-6, CM-6, AU-6 | Least privilege, configuration and evidence |
| SOC 2 | CC6.1, CC7.2 | Access and monitoring |
Sources: code.claude.com · platform.claude.com · platform.claude.com (checked 2026-11-05).
1.4 Reconcile Federal Deployment Guidance with Current Provider Routing
Profile Level: L3 (Run)
Description
For FISMA workloads, use Anthropic’s public-sector Code best-practice guide as architecture/governance context and validate every variable and supported service against current configuration docs. A commercial Enterprise posture result does not authorize a federal deployment.
Rationale
Why This Matters:
- The January 2026 FISMA PDF includes variables that differ from current CLI references.
- Provider inference routing, endpoint permissions, logging and local tool execution remain separate responsibilities.
Attack Prevented: Unapproved inference routing, silent telemetry gaps and weak federal-workload access boundaries.
ClickOps Implementation
- Read the Claude Code public-sector guide and agency authorization boundary. Confirm an authorized Bedrock GovCloud or Google Assured Workloads service/region/model with the platform owner; no region string alone proves authorization.
- Deploy Code through managed endpoints with narrow IAM/SSO/MFA, reviewed tool/MCP access, approved egress and version/integrity/drift checks. Use provider audit logging and protected customer OTel where permitted; distinguish essential inference from optional telemetry.
- Use current documented
AWS_PROFILEwithCLAUDE_CODE_USE_BEDROCK, orANTHROPIC_VERTEX_PROJECT_ID/CLOUD_ML_REGIONwithCLAUDE_CODE_USE_VERTEX. The old PDF’sCLAUDE_CODE_AWS_PROFILEandCLAUDE_CODE_DISABLE_TELEMETRYexamples must not be copied as current configuration; current telemetry control isDISABLE_TELEMETRYor the documented nonessential-traffic switch. - Validate the actual provider, region/model and logs in an approved pilot. Keep commercial API, cloud-provider API and Claude for Government (
claude.fedstart.com) as different offerings; use the hub’s Government source appendix for the latter.
Code Implementation
Code Pack: Config
{
"env": {
"CLAUDE_CODE_USE_BEDROCK": "1",
"AWS_PROFILE": "REPLACE-WITH-APPROVED-GOVCLOUD-PROFILE",
"AWS_REGION": "us-gov-west-1",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
// Separate choice, not merged with the Bedrock example. Use current authorized region/model.
{
"env": {
"CLAUDE_CODE_USE_VERTEX": "1",
"ANTHROPIC_VERTEX_PROJECT_ID": "REPLACE-WITH-APPROVED-PROJECT",
"CLOUD_ML_REGION": "REPLACE-WITH-APPROVED-REGION",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
Validation & Testing
- Inspect the managed environment without printing credentials; compare effective routing with provider-side request records and authentication policy.
- Verify baseline integrity, drift alerts and incident evidence retention; offline payload parsing earns no FISMA or FedRAMP assurance.
Expected result: The documented boundary is exercised and any unsupported surface or exception is recorded.
Operational Impact
No cloud sign-in, authorization change, model call or credential creation is performed here. The payload examples are mutually exclusive choices and require a current approved model/region deployment.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls | 4.1, 6.8 | Secure configuration and role-based access |
| NIST 800-53 | AC-6, CM-6, AU-6 | Least privilege, configuration and evidence |
| SOC 2 | CC6.1, CC7.2 | Access and monitoring |
Sources: code.claude.com · code.claude.com · code.claude.com · trust.anthropic.com (checked 2026-11-05).
2. Extensions & Supply Chain
2.1 Control MCP Server Access
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | CM-7, SA-9 |
| SOC 2 | CC6.6, CC9.2 |
Description
Restrict which Model Context Protocol (MCP) servers Claude Code can connect to using a managed MCP configuration file or allowlist/denylist settings. MCP servers extend Claude Code’s capabilities by providing additional tools — an uncontrolled MCP server can introduce arbitrary tool access.
Rationale
Why This Matters:
- MCP servers can provide Claude Code with tools to access databases, APIs, cloud services, and more
- A malicious or misconfigured MCP server can grant unintended access to sensitive systems
- The
managed-mcp.jsonfile provides exclusive control — when present, users cannot add their own MCP servers - Deny rules in
deniedMcpServersalways take precedence over allow rules
Attack Prevented: Supply chain attack via malicious MCP server, unauthorized system access, data exfiltration through MCP tools
Prerequisites
- MDM deployment capability (for managed-mcp.json) or managed settings access
- Inventory of approved MCP servers and their security posture
ClickOps Implementation
Step 1: Inventory MCP Servers
- Survey development teams for MCP servers in use
- Assess each server’s security posture (source, maintainer, permissions granted)
- Create an approved list
Step 2: Deploy Managed MCP Configuration
Deploy managed-mcp.json to the OS-specific path:
| OS | Path |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-mcp.json |
| Linux / WSL | /etc/claude-code/managed-mcp.json |
| Windows | C:\Program Files\ClaudeCode\managed-mcp.json |
When this file exists, it takes exclusive control — users cannot add, modify, or use any MCP servers other than those defined in this file.
Step 3: Alternative — Allowlist/Denylist via Managed Settings
- Add
allowedMcpServersto managed settings with approved server names, commands, or URLs - Add
deniedMcpServersfor explicitly blocked servers (deny always wins) - URL wildcards are supported (e.g.,
https://*.company.com/*) - Set
allowManagedMcpServersOnly: truein the same managed source. Without it, allowlists from every settings scope merge — including a user’s own~/.claude/settings.json— so a user can broaden what your allowlist permits. Denylists merge from every scope regardless
Time to Complete: ~15 minutes
Code Implementation
Code Pack: Config
// managed-mcp.json — exclusive MCP server control
// Reference: code.claude.com/docs/en/mcp#managed-mcp-configuration
// When this file exists at the system path, it takes exclusive control:
// users cannot add, modify, or use any MCP servers not defined here.
// Deploy alongside managed-settings.json at the same OS-specific path:
// macOS: /Library/Application Support/ClaudeCode/managed-mcp.json
// Linux: /etc/claude-code/managed-mcp.json
// Windows: C:\Program Files\ClaudeCode\managed-mcp.json
//
// Note: Server-managed settings cannot distribute MCP server configs —
// this file must be deployed via MDM, Group Policy, or Ansible.
{
"mcpServers": {
"approved-github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
},
"approved-postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "${APPROVED_DB_URL}"
}
}
}
}
// MCP server allowlist/denylist via managed-settings.json
// Reference: code.claude.com/docs/en/settings#mcp-configuration-managed
// Use this when you want guardrails without exclusive MCP control.
// Deny rules always take precedence over allow rules.
// undefined = no restrictions; empty array [] = complete lockdown.
// allowManagedMcpServersOnly: true makes this allowlist authoritative. Without
// it, allowlists from every settings scope merge (including a user's own
// ~/.claude/settings.json), so a user can broaden what this list permits.
// Denylists merge from every scope regardless.
// Reference: code.claude.com/docs/en/managed-mcp
{
"allowedMcpServers": [
{"serverName": "github"},
{"serverName": "memory"},
{"serverName": "postgres"}
],
"deniedMcpServers": [
{"serverName": "filesystem"},
{"serverName": "shell"},
{"serverName": "puppeteer"}
],
"allowManagedMcpServersOnly": true,
"enableAllProjectMcpServers": false
}
// Granular control over project-level MCP servers (.mcp.json)
// Reference: code.claude.com/docs/en/settings
// By default, Claude Code prompts users to approve .mcp.json servers.
// enableAllProjectMcpServers: true auto-approves all project MCP servers.
// For fine-grained control, use enabledMcpjsonServers / disabledMcpjsonServers
// to pre-approve or block specific server names from .mcp.json files.
// Both lists take plain server-name strings (not {"serverName": ...} objects,
// which fail validation and are dropped from a managed file).
{
"enableAllProjectMcpServers": false,
"enabledMcpjsonServers": ["github", "memory"],
"disabledMcpjsonServers": ["filesystem", "shell", "puppeteer", "everything"],
"allowManagedMcpServersOnly": true
}
Code Pack: API Script
# Validate MCP configuration on this machine
if [[ -n "${HTH_CLAUDE_MANAGED_DIR:-}" ]]; then
MANAGED_DIR="${HTH_CLAUDE_MANAGED_DIR}"
else
case "$(uname -s)" in
Darwin) MANAGED_DIR="/Library/Application Support/ClaudeCode" ;;
Linux) MANAGED_DIR="/etc/claude-code" ;;
MINGW*|MSYS*|CYGWIN*) MANAGED_DIR="C:/Program Files/ClaudeCode" ;;
*) fail "7.3 Unknown OS — cannot locate the managed settings directory"; summary; exit 1 ;;
esac
fi
# managed-mcp.json takes exclusive control: users cannot add other servers
EXCLUSIVE=0
MCP_PATH="${MANAGED_DIR}/managed-mcp.json"
if [[ -f "${MCP_PATH}" ]]; then
if jq -e '(.mcpServers | type) == "object"' "${MCP_PATH}" >/dev/null 2>&1; then
EXCLUSIVE=1
SERVER_COUNT=$(jq '.mcpServers | length' "${MCP_PATH}")
pass "7.3 managed-mcp.json deployed — exclusive control, ${SERVER_COUNT} approved MCP servers"
jq -r '.mcpServers | keys[]' "${MCP_PATH}" | while IFS= read -r server; do info " - ${server}"; done
else
fail "7.3 managed-mcp.json exists but is not a JSON object with an mcpServers map"
fi
else
info "7.3 No managed-mcp.json — checking the allowlist/denylist in managed settings"
fi
# Managed settings: base file first, then drop-ins in alphabetical order
SOURCES=()
if [[ -f "${MANAGED_DIR}/managed-settings.json" ]]; then
SOURCES+=("${MANAGED_DIR}/managed-settings.json")
fi
LC_COLLATE=C
for f in "${MANAGED_DIR}/managed-settings.d/"*.json; do
if [[ -f "${f}" ]]; then SOURCES+=("${f}"); fi
done
if [[ ${#SOURCES[@]} -eq 0 ]]; then
if [[ ${EXCLUSIVE} -eq 1 ]]; then
info "7.3 No managed settings file — MCP is governed by managed-mcp.json alone"
else
fail "7.3 No managed-mcp.json and no managed settings — MCP is uncontrolled"
fi
summary
if [[ ${FAILED} -eq 0 ]]; then exit 0; fi
exit 1
fi
MERGED=$(jq -s '
def m($a; $b):
if ($a|type) == "object" and ($b|type) == "object" then
reduce ($b|keys[]) as $k ($a; .[$k] = m($a[$k]; $b[$k]))
elif ($a|type) == "array" and ($b|type) == "array" then ($a + $b | unique)
else $b end;
reduce .[] as $x ({}; m(.; $x))' "${SOURCES[@]}") || {
fail "7.3 A managed settings file is not valid JSON"; summary; exit 1
}
merged() { printf '%s' "${MERGED}" | jq "$@"; }
ALLOWED=$(merged '.allowedMcpServers // [] | length')
DENIED=$(merged '.deniedMcpServers // [] | length')
LOCKED=$(merged -r '.allowManagedMcpServersOnly // false')
info "MCP allowlist: ${ALLOWED} servers, denylist: ${DENIED} servers, allowManagedMcpServersOnly: ${LOCKED}"
if [[ ${EXCLUSIVE} -eq 0 ]] && [[ "${ALLOWED}" -eq 0 ]] && [[ "${DENIED}" -eq 0 ]]; then
fail "7.3 No managed-mcp.json, allowlist, or denylist — all MCP servers are permitted"
fi
if [[ "${DENIED}" -gt 0 ]]; then
pass "7.3 MCP deny rules configured (${DENIED} servers blocked)"
fi
if [[ "${ALLOWED}" -gt 0 ]]; then
if [[ "${LOCKED}" == "true" ]]; then
pass "7.3 MCP allowlist configured and authoritative (${ALLOWED} servers approved)"
elif [[ ${EXCLUSIVE} -eq 1 ]]; then
info "7.3 allowedMcpServers is set without allowManagedMcpServersOnly; managed-mcp.json still holds exclusive control"
else
fail "7.3 MCP allowlist is soft — without allowManagedMcpServersOnly: true, users can broaden it in ~/.claude/settings.json"
fi
elif [[ "${DENIED}" -gt 0 ]] && [[ ${EXCLUSIVE} -eq 0 ]]; then
warn "7.3 Denylist only — any MCP server not on the denylist is permitted"
fi
if [[ "${LOCKED}" == "true" ]] && [[ "${ALLOWED}" -eq 0 ]] && [[ ${EXCLUSIVE} -eq 0 ]]; then
warn "7.3 allowManagedMcpServersOnly is true but no managed allowedMcpServers list was found in these files"
fi
# jq's // treats false as missing, so read the boolean explicitly
AUTO_APPROVE=$(merged -r 'if .enableAllProjectMcpServers == null then "not set" else .enableAllProjectMcpServers end')
if [[ "${AUTO_APPROVE}" == "true" ]]; then
warn "7.3 enableAllProjectMcpServers is true — project MCP servers auto-approved"
elif [[ "${AUTO_APPROVE}" == "false" ]]; then
pass "7.3 Project MCP servers require explicit approval"
fi
Validation & Testing
- Verify managed-mcp.json is deployed (if using exclusive control)
- Attempt to add an unapproved MCP server — should be blocked
- Verify approved MCP servers connect successfully
- Test deny rule against a specific server name — should be blocked
Expected result: Only approved MCP servers can be used; all others are blocked when managed-mcp.json is deployed or allowManagedMcpServersOnly is true (an allowlist without it can be broadened by users)
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | Medium | Developers can only use pre-approved MCP servers |
| System Performance | None | MCP config is loaded once at startup |
| Maintenance Burden | Medium | Approved list needs updates as new servers are adopted |
| Rollback Difficulty | Easy | Remove managed-mcp.json or update allowlist |
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.6, CC9.2 | System boundaries; vendor risk management |
| NIST 800-53 | CM-7, SA-9 | Least functionality; external system services |
| ISO 27001 | A.15.1.2 | Addressing security within supplier agreements |
2.2 Lock Down Hooks and Plugins
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | CM-7, SI-7 |
| SOC 2 | CC6.1, CC8.1 |
Description
Restrict the Claude Code extensibility surface by enforcing managed-only hooks, controlling plugin marketplace access, and allowlisting HTTP hook destinations. Hooks execute at lifecycle events (PreToolUse, PostToolUse, SessionStart, etc.) and can run arbitrary commands — a malicious hook can exfiltrate data or modify tool behavior. Plugins extend Claude Code with skills, agents, commands, and hooks from external sources.
Rationale
Why This Matters:
- Hooks, plugin MCP/LSP servers and background executables can run host code. The vendor’s plugin security guidance makes clear that tool permission and sandbox rules do not contain plugin-owned code. Review the source and updates, restrict installation sources and keep managed hook policy separate from HTTP destination policy. An allowlisted marketplace is a trust decision, not a code-safety guarantee.
Attack Prevented: Unreviewed executable plugin or hook code acquiring host credentials or changing tool behavior.
Prerequisites
- Managed settings deployment (Control 1.1)
- Inventory of approved internal plugin marketplaces
- List of authorized HTTP webhook endpoints
ClickOps Implementation
Step 1: Lock Hooks to Managed-Only
- Navigate to: claude.ai → Organization settings → Claude Code → Managed settings (claude.ai/admin-settings/claude-code)
- Add
"allowManagedHooksOnly": true— blocks all user, project, and plugin hooks - Define any required hooks directly in managed settings under the
"hooks"key
Step 2: Restrict Plugin Marketplaces
- Add
"strictKnownMarketplaces"with your approved marketplace repos only - Set to empty array
[]to block all marketplace plugin installations - Add
"blockedMarketplaces"for explicitly banned sources — checked before download. Review plugin-owned hooks, MCP/LSP servers, background processes, JavaScript and bundled executables as host code: Claude tool permissions and the sandbox do not contain that code. Review updates and marketplace auto-update behavior, not only the initial install. - Optionally set
"pluginTrustMessage"with org-specific guidance for developers
Step 3: Allowlist HTTP Hook URLs
- Add
"allowedHttpHookUrls": ["https://hooks.example.com/*"]with approved webhook endpoints - Set to empty array
[]to block all HTTP hooks - Add
"httpHookAllowedEnvVars": ["HOOK_AUTH_TOKEN"]to restrict which env vars hooks can access
Time to Complete: ~15 minutes
Code Implementation
Code Pack: Config
// L2 Walk hook and plugin lockdown.
// Only managed hooks execute; plugin installs restricted to
// approved marketplaces; HTTP hooks limited to internal URLs.
{
"allowManagedHooksOnly": true,
"allowedHttpHookUrls": [
"https://hooks.example.com/*",
"https://security.example.com/api/*"
],
"httpHookAllowedEnvVars": [
"HOOK_AUTH_TOKEN"
],
"strictKnownMarketplaces": [
{
"source": "github",
"repo": "your-org/approved-plugins"
}
],
"blockedMarketplaces": [
{
"source": "github",
"repo": "untrusted-org/plugins"
}
],
"pluginTrustMessage": "Only plugins approved by the security team are permitted. Contact #security-approvals for new plugin requests."
}
// L3 Run — no plugins, no hooks, no HTTP hook URLs.
// Complete lockdown of extensibility surface.
{
"allowManagedHooksOnly": true,
"disableAllHooks": false,
"allowedHttpHookUrls": [],
"httpHookAllowedEnvVars": [],
"strictKnownMarketplaces": [],
"blockedMarketplaces": [],
"pluginTrustMessage": "Plugin installation is disabled by organizational policy.",
"allowManagedMcpServersOnly": true,
"allowManagedPermissionRulesOnly": true
}
Validation & Testing
- Create a hook in
.claude/settings.json— verify it does not execute whenallowManagedHooksOnlyis true - Attempt to install a plugin from a non-approved marketplace — should be blocked
- Verify
blockedMarketplacesentries are rejected before download - Create an HTTP hook targeting a non-allowlisted URL — verify it is blocked
- Verify
pluginTrustMessageappears during plugin trust prompts
Expected result: Only managed hooks execute; plugins limited to approved sources; HTTP hooks restricted to approved endpoints
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | High | Developers cannot install arbitrary plugins or create hooks |
| System Performance | None | Settings evaluated once at startup |
| Maintenance Burden | Medium | Approved marketplace and webhook lists need updates |
| Rollback Difficulty | Easy | Remove restrictive settings from managed config |
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1, CC8.1 | Logical access security; change management |
| NIST 800-53 | CM-7, SI-7 | Least functionality; software integrity |
| ISO 27001 | A.12.5.1, A.12.6.1 | Software installation controls; technical vulnerability management |
3. Execution Isolation
3.1 Enforce Bash Sandbox Isolation
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | SC-39, CM-7 |
| SOC 2 | CC6.1, CC6.8 |
Description
Enable OS-level bash command sandboxing to isolate Claude Code’s subprocess execution. The sandbox restricts filesystem access to the current working directory, routes network traffic through a validating proxy with domain allowlisting, and enforces restrictions at the kernel level using Seatbelt (macOS) or bubblewrap (Linux/WSL2). Set failIfUnavailable: true to prevent Claude Code from starting if sandboxing cannot be established.
Rationale
Why This Matters:
- Without sandboxing, Claude Code bash commands have full access to the developer’s filesystem and network
- A prompt injection or confused agent could read credentials (
~/.aws/credentials,~/.ssh/), exfiltrate data viacurl, or modify system files - OS isolation constrains covered processes, but excluded commands, supported-platform limitations, plugin host code and in-process tools remain separate boundaries
sandbox.network.allowManagedDomainsOnly: trueprevents developers from approving new network domains at runtime
Attack Prevented: Credential theft via filesystem access, data exfiltration via network, unauthorized file modification, supply chain attacks via package manager hijacking
Prerequisites
- Managed settings deployment (Control 1.1)
- macOS: Seatbelt available (built-in on all supported macOS versions)
- Linux/WSL2: bubblewrap (
bwrap) andsocatinstalled - Inventory of required network domains for development workflows
ClickOps Implementation
Step 1: Enable Sandbox
- Navigate to: claude.ai → Organization settings → Claude Code → Managed settings (claude.ai/admin-settings/claude-code)
- Add
"sandbox": { "enabled": true, "failIfUnavailable": true }to your managed settings JSON - Set
sandbox.allowUnsandboxedCommandstofalse(inside thesandboxobject) to close thedangerouslyDisableSandboxescape hatch
Step 2: Configure Network Allowlist
- Identify required domains:
github.com, package registries (*.npmjs.org,pypi.org), internal services - Add to
sandbox.network.allowedDomainsarray - Set
sandbox.network.allowManagedDomainsOnlytotrueto prevent user overrides — nested undersandbox.network; the settings root accepts unknown keys, so a root-level copy is silently ignored - Non-allowed domains are blocked automatically without prompting
Step 3: Configure Filesystem Restrictions
- Add sensitive paths to
sandbox.filesystem.denyRead:~/.aws/credentials,~/.ssh/id_*,~/.gnupg/ - Add critical system paths to
sandbox.filesystem.denyWrite:/etc,/usr/local/bin - Optionally set
sandbox.filesystem.allowManagedReadPathsOnlytotruefor L3 environments
Step 4: Install Linux Dependencies (if needed)
- On Ubuntu/Debian:
sudo apt install bubblewrap socat - On Fedora/RHEL:
sudo dnf install bubblewrap socat - Verify: Run
/sandboxin Claude Code — should report “Sandbox active”
Step 5: Account for Tools Outside the Sandbox
- Warning: The sandbox applies only to Bash, PowerShell, and Monitor commands and their child processes. In-process tools are outside its network proxy — the docs name
WebFetch(“in-process tools such as WebFetch still follow their permission rules”), andWebSearch, which is not a shell command either, is likewise governed by permission rules rather thansandbox.network.allowedDomains - If outbound data exfiltration is a concern, add
"WebFetch"and"WebSearch"to thepermissions.denylist in managed settings
Time to Complete: ~20 minutes
Code Implementation
Code Pack: Config
// L2 Walk sandbox configuration.
// Locks filesystem to CWD, denies sensitive credential paths,
// restricts network to approved domains only.
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"autoAllowBashIfSandboxed": false,
"allowUnsandboxedCommands": false,
"excludedCommands": [],
"filesystem": {
"denyRead": [
"~/.aws/credentials",
"~/.ssh/id_*",
"~/.gnupg/",
"~/.config/gcloud/",
"~/.kube/config"
],
"denyWrite": [
"/etc",
"/usr/local/bin",
"~/.claude/managed-settings.json"
],
"allowWrite": [
"/tmp/build"
]
},
"network": {
"allowedDomains": [
"github.com",
"*.githubusercontent.com",
"*.npmjs.org",
"registry.yarnpkg.com",
"pypi.org",
"files.pythonhosted.org"
],
"allowManagedDomainsOnly": true,
"allowAllUnixSockets": false,
"allowUnixSockets": [],
"allowLocalBinding": false
},
"enableWeakerNestedSandbox": false
}
}
// L3 Run sandbox configuration.
// No unsandboxed commands, no excluded commands, strict filesystem
// and network lockdown. Suitable for regulated environments.
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"autoAllowBashIfSandboxed": false,
"allowUnsandboxedCommands": false,
"excludedCommands": [],
"filesystem": {
"denyRead": [
"~/.aws/",
"~/.ssh/",
"~/.gnupg/",
"~/.config/gcloud/",
"~/.kube/",
"~/.docker/config.json",
"~/.npmrc",
"~/.pypirc"
],
"allowManagedReadPathsOnly": true,
"denyWrite": [
"/etc",
"/usr",
"/var",
"~/.claude/"
],
"allowWrite": []
},
"network": {
"allowedDomains": [],
"allowManagedDomainsOnly": true,
"allowAllUnixSockets": false,
"allowUnixSockets": [],
"allowLocalBinding": false
},
"enableWeakerNestedSandbox": false,
"enableWeakerNetworkIsolation": false
}
}
Validation & Testing
- Run Claude Code with sandbox enabled — verify
/sandboxshows active status - Attempt to read
~/.aws/credentialsvia Claude Code — should be denied - Attempt to
curla non-allowlisted domain — should be blocked - Set
failIfUnavailable: trueand remove bubblewrap (Linux) — Claude Code should refuse to start - Verify
allowManagedDomainsOnlyprevents user domain approval prompts
Expected result: All bash commands execute in kernel-enforced sandbox; credential paths are unreadable; network limited to approved domains
Monitoring & Maintenance
Ongoing monitoring:
- Monitor for sandbox startup failures via OpenTelemetry metrics
- Track domain approval requests that hit the managed-only block
Maintenance schedule:
- Monthly: Review and update
allowedDomainsas development tooling evolves - Quarterly: Audit
denyRead/denyWritepaths against new credential storage patterns
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | Medium | Some commands may fail if domains not allowlisted |
| System Performance | Low | Proxy latency depends on the workload and deployment |
| Maintenance Burden | Medium | Domain allowlist needs updating as tooling changes |
| Rollback Difficulty | Easy | Set sandbox.enabled: false in managed settings |
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1, CC6.8 | Logical access security; system boundaries |
| NIST 800-53 | SC-39, CM-7 | Process isolation; least functionality |
| ISO 27001 | A.13.1.3 | Network segregation |
3.2 Deploy External Sandbox Tooling
Profile Level: L3 (Run)
Description
Use an independently reviewed execution boundary around Code when required. The vendor documents development containers as one deployment option; the container does not replace Code permissions, managed policy or controlled egress.
Rationale
Why This Matters:
- Bind-mounted project files remain writable on the host, and any secret visible in the container can be read by code inside it.
- Bypass-permission mode removes human review even in a container. A broadly trusted network host can still carry exfiltration.
Attack Prevented: Host credential exposure and excessive file/network access from agent-driven execution.
ClickOps Implementation
- Follow the vendor’s development-container setup. In VS Code, review the repository configuration before Command Palette → Dev Containers: Rebuild Container or Reopen in Container. A build can execute repository code; use a trusted reviewed image/repository.
- Inspect host mounts and credential injection. Do not mount host SSH/cloud credential directories. Use repository-scoped or short-lived credentials only for the needed integration.
- Deliver the Code managed baseline independently of an editable repository Dockerfile. The vendor notes that a repository writer can remove Dockerfile-based policy; verify the selected source in Code rather than assuming a copied JSON file enforces it.
- Add reviewed egress controls. The vendor’s reference container is an example, not a maintained security base image; its firewall needs additional capabilities and those are not required by Code itself. Choose the organization’s actual enforcement layer, including 3.1, and verify excluded tools separately.
- Keep bypass mode disabled where human permission review is required. A container does not make an untrusted repository safe or establish a complete isolation guarantee.
Code Implementation
The pack now supplies the documented native starting configuration. It replaces a legacy script that unconditionally launched multiple wrappers and a rollback action; no install, restore or sandbox launch is performed by the new payload. Third-party wrapper flags are not represented as currently verified Anthropic interfaces.
Code Pack: Config
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
},
"containerEnv": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
Validation & Testing
- Review the final image, mounts, credentials and network policy before an approved build; the feature tag pins its install script, not the Code CLI release.
- In an approved pilot, test access to mounted files, denied host secrets and allowed/denied network destinations. Verify Code policy and local/provider logs.
Expected result: The deployment’s actual host, filesystem, credential and network boundaries are documented and independently checked; parsing the sample is not a sandbox security proof.
Operational Impact
Containers change local persistence, networking and editor workflows. Preserve needed incident evidence before rebuild/cleanup. Generic container/host infrastructure hardening remains outside this guide; use the organization’s existing baseline.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| NIST 800-53 | SC-39, SC-7, CM-6 | Isolation, network and application policy |
| SOC 2 | CC6.1, CC6.8 | Access and system boundaries |
4. Threat Defense
4.1 Defend Against Prompt Injection and Rules File Attacks
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | SI-10, SI-7 |
| SOC 2 | CC6.1, CC7.2 |
Description
Implement defenses against prompt injection attacks that target Claude Code through repository files. Attackers embed malicious instructions in CLAUDE.md, AGENTS.md, SKILL.md, and other rules files that Claude Code reads as context. These “rules file backdoor” attacks can instruct the AI to exfiltrate data, disable safety features, or execute malicious commands. Use the local scanner as a review aid alongside enforced permissions, credential isolation and managed plugin policy.
Rationale
Why This Matters:
- Instruction files, retrieved content, tool output and plugins can carry adversarial instructions. The vendor security guidance recommends reviewing untrusted content, using permissions and isolation, and keeping the client updated. Static pattern scanners surface review candidates but cannot prove prompt-injection absence; legitimate multilingual files can trigger character heuristics. The guide does not rely on an unrefreshed third-party incident catalog for this control.
Attack Prevented: Instructions in untrusted content steering tools toward disclosure or destructive changes.
Prerequisites
- Git pre-commit hook infrastructure or CI/CD pipeline
- Familiarity with Claude Code rules file locations (CLAUDE.md, AGENTS.md,
.claude/directory)
ClickOps Implementation
Step 1: Scan Rules Files Before Trusting Repositories
- Before opening any new repository with Claude Code, review
CLAUDE.mdand.claude/directory contents - Look for: encoded payloads (base64), invisible Unicode characters, instruction override patterns, network exfiltration commands
- Check for hidden characters:
LC_ALL=C cat -v CLAUDE.md | grep -n 'M-'lists every line containing a non-ASCII byte (zero-width U+200B/U+200C/U+200D, BOM U+FEFF, bidi controls such as U+202E); review each hit. WithoutLC_ALL=C,cat -vin a UTF-8 locale passes multibyte characters through unmarked and the check finds nothing
Step 2: Require Reviewed Hooks
- Keep administrative hooks in managed settings and apply the hook/marketplace controls in 2.2.
- Review hook source, destinations, credential access and update provenance before deployment. Do not install third-party plugins simply to satisfy this control.
- Treat hook output as an additional signal; independently test actual permissions and filesystem/network isolation.
Step 3: Deploy Rules File Scanning in CI
- Add the HTH rules file scanner script as a pre-commit hook or CI step
- The scanner checks for: data exfiltration patterns, encoded payloads, invisible Unicode, instruction override attempts, safety bypass requests
- Configure to run on every PR that modifies
CLAUDE.md,AGENTS.md, or.claude/directory
Step 4: Review Findings Before Admitting Content
- Record an owner, source revision and approval for repository instruction files, skills and plugins. Review changed linked files and executable plugin components.
- Triage scanner findings manually; do not convert an empty scan into a security pass. Keep production credentials and sensitive host paths out of the test environment.
Time to Complete: ~30 minutes
Code Implementation
Code Pack: API Script
set -euo pipefail
TARGET_DIR="${1:-.}"
FINDINGS=0
# A typo'd path must not read as a clean scan
if [ ! -d "$TARGET_DIR" ]; then
echo "ERROR: scan target is not a directory: $TARGET_DIR" >&2
exit 2
fi
echo "=== Claude Code Rules File Security Scanner ==="
echo "Scanning: ${TARGET_DIR}"
echo ""
# Patterns commonly found in prompt injection attacks against AI coding agents.
# Sources: Pillar Security "Rules File Backdoor" (2025), Snyk ToxicSkills (2026),
# Lasso Security indirect prompt injection research (2026).
SUSPICIOUS_PATTERNS=(
# Data exfiltration via network commands
'curl\s+.*\$'
'wget\s+.*\$'
'fetch\(.*\$'
'nc\s+-'
# Encoded payloads hiding instructions
'base64\s+--decode'
'eval\s*\('
'exec\s*\('
# Invisible Unicode characters used to hide instructions. $'...' (ANSI-C
# quoting) turns each escape into the raw UTF-8 bytes; grep treats a plain
# '\xe2...' string as literal text and would never match.
$'\xe2\x80\x8b' # zero-width space (U+200B)
$'\xe2\x80\x8c' # zero-width non-joiner (U+200C)
$'\xe2\x80\x8d' # zero-width joiner (U+200D)
$'\xef\xbb\xbf' # BOM / zero-width no-break space (U+FEFF)
# Bidirectional controls that reorder how text displays (Trojan Source)
$'\xe2\x80\xaa' # left-to-right embedding (U+202A)
$'\xe2\x80\xab' # right-to-left embedding (U+202B)
$'\xe2\x80\xac' # pop directional formatting (U+202C)
$'\xe2\x80\xad' # left-to-right override (U+202D)
$'\xe2\x80\xae' # right-to-left override (U+202E)
$'\xe2\x81\xa6' # left-to-right isolate (U+2066)
$'\xe2\x81\xa7' # right-to-left isolate (U+2067)
$'\xe2\x81\xa8' # first strong isolate (U+2068)
$'\xe2\x81\xa9' # pop directional isolate (U+2069)
# Instruction override attempts
'ignore\s+(all\s+)?previous\s+instructions'
'disregard\s+(all\s+)?prior'
'override\s+system\s+prompt'
'you\s+are\s+now\s+in\s+.*mode'
'new\s+instructions:'
'IMPORTANT:\s*override'
# Exfiltration via MCP or tool abuse
'mcp.*install.*--force'
'plugin.*install.*--trust'
# Requests to disable safety
'dangerously-skip-permissions'
'bypass.*permission'
'disable.*safety'
'allow.*all.*commands'
)
# Files to scan: CLAUDE.md, AGENTS.md, skills, hooks, and plugin configs
SCAN_FILES=()
while IFS= read -r -d '' file; do
SCAN_FILES+=("$file")
done < <(find "$TARGET_DIR" \
\( -name "CLAUDE.md" -o -name "AGENTS.md" -o -name "GEMINI.md" \
-o -name "COPILOT.md" -o -name "*.skill.md" -o -name "SKILL.md" \
-o -path "*/.claude/settings.json" \
-o -path "*/.claude/settings.local.json" \
-o -path "*/.claude/agents/*.md" \
-o -path "*/.claude/commands/*.md" \) \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" \
-print0 2>/dev/null)
if [ ${#SCAN_FILES[@]} -eq 0 ]; then
echo "No rules files found to scan."
exit 0
fi
echo "Found ${#SCAN_FILES[@]} rules file(s) to scan."
echo ""
for file in "${SCAN_FILES[@]}"; do
echo "--- Scanning: ${file} ---"
for pattern in "${SUSPICIOUS_PATTERNS[@]}"; do
# LC_ALL=C makes grep match raw bytes, so multibyte patterns match in any locale
matches=$(LC_ALL=C grep -cEi "$pattern" "$file" 2>/dev/null || true)
if [ "${matches:-0}" -gt 0 ]; then
# cat -v renders invisible bytes visibly (e.g. U+200B prints as M-bM-^@M-^K)
echo " [ALERT] Pattern matched ($matches occurrences): $(printf '%s' "$pattern" | LC_ALL=C cat -v)"
{ LC_ALL=C grep -nEi "$pattern" "$file" 2>/dev/null || true; } | head -3 | LC_ALL=C cat -v | while read -r line; do
echo " $line"
done
FINDINGS=$((FINDINGS + matches))
fi
done
done
echo ""
echo "=== Scan Complete ==="
echo "Total suspicious patterns found: ${FINDINGS}"
if [ "$FINDINGS" -gt 0 ]; then
echo ""
echo "ACTION REQUIRED: Review flagged patterns before trusting this repository."
echo "Not all findings are malicious — review context carefully."
echo "See: howtoharden.com/guides/anthropic-claude/#77"
exit 1
fi
echo "No suspicious patterns detected."
exit 0
Validation & Testing
- Run the local scanner on reviewed fixtures, including an injected-instruction example and a legitimate multilingual file. Review false positives and missed cases.
- Verify the actual managed hooks, permissions and isolation with approved allowed/denied probes. A scanner exit zero is only a heuristic result.
- Confirm newly changed instruction, skill, plugin and linked files require code review; block release until unresolved findings have an owner and disposition.
Expected result: Content and executable extensions receive evidenced review, with separate enforcement checks; no claim of complete injection detection.
Operational Impact
Heuristic scans add review work and can miss novel attacks. Keep fixtures current and review changes to imported files and plugin versions.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1, CC7.2 | Logical access security; system monitoring |
| NIST 800-53 | SI-10, SI-7 | Information input validation; software integrity |
| ISO 27001 | A.12.2.1, A.14.2.8 | Controls against malware; system security testing |
4.2 Harden Claude Code in CI/CD Pipelines
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | SA-11, SA-15 |
| SOC 2 | CC7.1, CC8.1 |
Description
Scope Claude Code credentials, tool permissions and repository access when using GitHub Actions. Run only reviewed content in a protected environment. The pack provides an explicitly dispatched native action example; it never enables an automatic secret-bearing review of arbitrary pull requests. Apply your existing runner network policy separately.
Rationale
Why This Matters:
- A CI agent processes repository-controlled content while holding automation credentials. Use the official GitHub Actions security and authentication guidance, protect the runner’s secrets and constrain granted tools. A network monitor in audit mode detects traffic but does not block it. A prompt or an allowlisted tool name does not make untrusted PR code safe to execute.
Attack Prevented: Credential disclosure or unauthorized writes by a CI agent processing repository content.
Prerequisites
- GitHub repository administration and an existing approved Claude API credential.
- A protected environment named claude-code-reviewed, configured with required reviewers and an environment-scoped API secret before using the sample.
- A reviewed commit and instruction/plugin inventory; no credential-bearing execution of untrusted fork code.
ClickOps Implementation
- In GitHub → repository Settings → Environments, protect the review environment and restrict its branches/reviewers. Store the existing credential there. The YAML alone does not configure protection.
- In Actions, explicitly dispatch the workflow with a reviewed immutable commit. Keep checkout credentials unpersisted and job permissions read-only for log-only analysis.
- Review the pinned official action and updates. Supply an explicit least-privilege GitHub token or use the separately documented GitHub App authentication; the latter requires its documented OIDC permission. Do not run the app installer as a validation step.
- Keep hooks disabled for this restricted example, grant only required tools and deny shell, write and external-fetch tools. Review skill/plugin code separately; tool rules do not contain plugin-owned host processes.
- Apply the organization’s existing runner credential and outbound network policy. Monitoring in audit mode records traffic; it is not a blocking firewall. Additional endpoint needs vary by provider, model, dependencies and optional features.
- If adopting hosted Code Review or Claude Security, review their separate repository access, owner capabilities, data/retention and inference-hook exclusions; this workflow does not configure those products.
Time to Complete: ~30 minutes plus protected-environment review.
Code Implementation
Code Pack: Config
name: Reviewed Claude Code analysis
on:
workflow_dispatch:
inputs:
reviewed_sha:
description: Reviewed immutable commit (40 lowercase hex characters)
required: true
type: string
permissions: {}
jobs:
analyze:
runs-on: ubuntu-latest
environment: claude-code-reviewed
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
issues: read
steps:
- name: Validate selected revision
env:
REVIEWED_SHA: ${{ inputs.reviewed_sha }}
run: '[[ "$REVIEWED_SHA" =~ ^[a-f0-9]{40}$ ]]'
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.reviewed_sha }}
persist-credentials: false
fetch-depth: 1
- uses: anthropics/claude-code-action@f9fffe565bf81a7fcadb308a9d688a74e3b029f7 # v1 resolved 2026-11-05
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
prompt: Read the reviewed repository and report security observations in the workflow log. Do not execute repository code or publish findings.
settings: '{"disableAllHooks":true}'
claude_args: >-
--allowedTools "Read,Glob,Grep"
--disallowedTools "Bash,Write,Edit,NotebookEdit,WebFetch,WebSearch,Agent"
--max-turns 10
Validation & Testing
- Verify the environment really requires an authorized reviewer and cannot be bypassed by the workflow’s trigger/branch configuration.
- In an isolated pilot, select a reviewed fixture commit; confirm invalid revisions fail and that the action uses the pinned SHA, read-only token and expected tool settings.
- Verify denied shell/write/network operations cannot execute, no comment or repository write occurs, and the max-turn/time limits bound the run. Test the actual runner network policy independently.
- Confirm secrets are neither printed nor included in uploaded logs/artifacts; review instructions and dependencies again after changes.
Expected result: A deliberately approved run analyzes reviewed content with restricted authority. A successful workflow or syntax check is not proof of prompt-injection resistance.
Operational Impact
A protected environment adds review latency. Preserve useful incident logs under an approved retention policy; rotate a compromised credential and stop active jobs rather than removing a monitoring step as rollback.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.1, CC8.1 | Vulnerability management; change management |
| NIST 800-53 | SA-11, SA-15 | Developer security testing; development process |
| ISO 27001 | A.14.2.1, A.14.2.8 | Secure development policy; system security testing |
5. Monitoring, Collaboration & Incident Response
5.1 Monitor Claude Code Developer Metrics
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| NIST 800-53 | AU-6, SI-4 |
| SOC 2 | CC7.2 |
Description
Use the Claude Code Analytics API (/v1/organizations/usage_report/claude_code) to monitor per-user developer activity including sessions, commits, pull requests, lines of code, tool acceptance rates, and cost by model. This endpoint provides daily granularity with per-user breakdowns.
Rationale
Why This Matters:
- Per-user metrics enable detection of anomalous Claude Code usage patterns
- Tool acceptance rates below 70% may indicate permission configuration issues or developer friction
- Cost attribution by user and model enables budget management — this API is Anthropic’s sanctioned path for per-user Claude Code cost, superseding per-key Usage API breakdowns
- Tracking commits and PRs created by Claude Code quantifies AI-assisted development impact
- Dimension fields sharpen detection:
terminal_type(e.g.vscode,iTerm.app,tmux— flag unexpected environments),customer_type(apivssubscription), andactor(user_actorwithemail_address, orapi_actorwithapi_key_name— correlate against your key inventory)
Attack Prevented: Unauthorized bulk code generation, cost abuse, shadow AI usage detection
Monitoring blind spot (verified 2026-08-15): this API only tracks Claude Code usage on the Claude API. Sessions routed through Claude in Amazon Bedrock, Microsoft Foundry, Google Cloud (Vertex AI), or Claude Platform on AWS are invisible to it. If those deployments are permitted, close the gap with the OpenTelemetry integration or provider-side logging — otherwise “shadow AI usage detection” is only true for one of your routing paths.
Prerequisites
- Admin API key on a Claude Console organization — the API is free for all organizations with Admin API access, covering both
customer_typeapi(pay-as-you-go) andsubscription(Pro/Team) customers; a Team/Enterprise plan is not the gate. Claude Enterprise (claude.ai) organizations’ Claude Code activity is reported by the Claude Enterprise Analytics API (Analytics key) instead, and Claude Platform on AWS is not covered - Monitoring infrastructure for alert thresholds; data lands with up to 1-hour delay, single-day queries via
starting_at(YYYY-MM-DD, UTC),limitmax 1000
ClickOps Implementation
Step 1: Review Usage in Console
- Navigate to: platform.claude.com/claude-code (the Claude Code usage page, separate from the general Usage page). It requires the UsageView permission, granted to the Developer, Billing, Admin, Owner, and Primary Owner roles
- Team and Enterprise (claude.ai) organizations use claude.ai/analytics/claude-code instead (Admins and Owners)
- Review per-user activity patterns
Step 2: Configure Alerts
- Set up daily automated checks using the API script below
- Configure alerts for:
- Users with unusually high session counts
- Tool acceptance rates below 70%
- Cost exceeding per-user thresholds
- Integrate with observability platform (Datadog, Grafana, etc.)
Step 3: Configure OpenTelemetry (Optional)
- Set environment variables in managed settings for OTel export
- Available metrics: sessions, LOC, PRs, commits, cost, tokens, code edit decisions, active time
- Available events: user prompts, tool results, API requests, API errors, tool decisions
Time to Complete: ~15 minutes (API setup) + integration time
Code Implementation
Code Pack: API Script
# Fetch per-user Claude Code analytics for a given day
# Usage: set REPORT_DATE (or ANTHROPIC_REPORT_DATE) to YYYY-MM-DD; defaults to yesterday (UTC)
REPORT_DATE="${REPORT_DATE:-${ANTHROPIC_REPORT_DATE:-$(date -u -d 'yesterday' '+%Y-%m-%d' 2>/dev/null || \
date -u -v-1d '+%Y-%m-%d' 2>/dev/null)}}"
if ! [[ "${REPORT_DATE}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
fail "7.4 REPORT_DATE must be YYYY-MM-DD (got: ${REPORT_DATE})"
summary; exit 1
fi
# GET one page of the report. The admin key reaches curl as a header file
# (-H @<(...)), never as an argument, so it never appears in `ps` output.
# Returns non-zero on a transport error, a timeout, or any non-200 status.
cc_get() {
local path="${1#${ANTHROPIC_API_BASE}}"
anthropic_get "${path}"
}
# Follow has_more / next_page until the whole day is retrieved (limit max 1000)
info "Fetching Claude Code analytics for ${REPORT_DATE}..."
ALL='[]'; PAGE=""; PAGES=0
while :; do
QUERY="starting_at=${REPORT_DATE}&limit=1000"
if [[ -n "${PAGE}" ]]; then
QUERY="${QUERY}&page=$(jq -rn --arg p "${PAGE}" '$p | @uri')"
fi
RESP=$(cc_get "${ANTHROPIC_API_BASE}/v1/organizations/usage_report/claude_code?${QUERY}") || {
fail "7.4 Failed to fetch Claude Code analytics (page $((PAGES + 1)))"
summary; exit 1
}
if ! printf '%s' "${RESP}" | jq -e '(.data | type) == "array"' >/dev/null 2>&1; then
fail "7.4 Analytics response has no data array"
summary; exit 1
fi
ALL=$(printf '%s\n%s\n' "${ALL}" "${RESP}" | jq -cs '.[0] + .[1].data')
PAGES=$((PAGES + 1))
[[ "$(printf '%s' "${RESP}" | jq -er 'if (.has_more|type)=="boolean" then .has_more|tostring else error("missing has_more") end')" == "true" ]] || break
PAGE=$(printf '%s' "${RESP}" | jq -r '.next_page // empty')
if [[ -z "${PAGE}" ]] || [[ ${PAGES} -ge 1000 ]]; then
fail "7.4 has_more=true without a usable next_page — refusing to report a partial day"
summary; exit 1
fi
done
ANALYTICS=$(printf '%s' "${ALL}" | jq -c '{data: .}')
an() { printf '%s' "${ANALYTICS}" | jq "$@"; }
RECORD_COUNT=$(an '.data | length')
info "Found ${RECORD_COUNT} user records for ${REPORT_DATE} (${PAGES} page(s))"
if [[ "${RECORD_COUNT}" -eq 0 ]]; then
fail "7.4 0 Claude Code records for ${REPORT_DATE} — no activity was captured; confirm the day had Claude Code use and the key is for the right organization, then re-run with that date"
summary; exit 1
fi
# Per-user summary (api_actor rows are identified by their API key name)
{
printf 'USER\tTERMINAL\tSESSIONS\tCOMMITS\tPRS\tLOC_ADD\tLOC_DEL\n'
an -r '.data[] | [
(.actor.email_address // .actor.api_key_name // "unknown"),
(.terminal_type // "n/a"),
(.core_metrics.num_sessions // 0),
(.core_metrics.commits_by_claude_code // 0),
(.core_metrics.pull_requests_by_claude_code // 0),
(.core_metrics.lines_of_code.added // 0),
(.core_metrics.lines_of_code.removed // 0)
] | @tsv'
} | column -t -s $'\t'
pass "7.4 Claude Code analytics retrieved"
# Analyze tool acceptance rates — low acceptance may indicate
# overly permissive settings or developer friction
info "Analyzing tool acceptance rates..."
{
printf 'USER\tTOOL\tACCEPTED\tREJECTED\tRATE\n'
an -r '.data[] |
(.actor.email_address // .actor.api_key_name // "unknown") as $user |
.tool_actions // {} | to_entries[] |
[$user, .key, (.value.accepted // 0), (.value.rejected // 0),
(if ((.value.accepted // 0) + (.value.rejected // 0)) > 0
then ((.value.accepted // 0) * 100 / ((.value.accepted // 0) + (.value.rejected // 0)) | floor | tostring) + "%"
else "n/a" end)] | @tsv'
} | column -t -s $'\t'
# Flag users with low acceptance rates (below 70%); only combinations with >5 decisions are assessed
ASSESSED=$(an '[.data[] | .tool_actions // {} | to_entries[] |
select(((.value.accepted // 0) + (.value.rejected // 0)) > 5)] | length')
LOW_ACCEPTANCE=$(an '[.data[] |
(.actor.email_address // .actor.api_key_name // "unknown") as $user |
.tool_actions // {} | to_entries[] |
{user: $user, tool: .key, accepted: (.value.accepted // 0), rejected: (.value.rejected // 0)} |
select((.accepted + .rejected) > 5) |
select((.accepted / (.accepted + .rejected)) < 0.7)]')
LOW_COUNT=$(printf '%s' "${LOW_ACCEPTANCE}" | jq 'length')
if [[ "${ASSESSED}" -eq 0 ]]; then
warn "7.4 No user/tool combination had more than 5 decisions — acceptance rates not assessed"
elif [[ "${LOW_COUNT}" -gt 0 ]]; then
warn "7.4 ${LOW_COUNT} of ${ASSESSED} user/tool combinations have <70% acceptance rate — review permission configuration"
else
pass "7.4 All ${ASSESSED} assessed user/tool combinations have >=70% acceptance"
fi
# Cost breakdown by model across all Claude Code users
info "Cost breakdown by model:"
an -r '[.data[].model_breakdown[]? |
{model: .model, cost: ((.estimated_cost.amount // 0) / 100)}] |
group_by(.model) | .[] |
{model: .[0].model, total_cost: ([.[].cost] | add | . * 100 | round / 100)} |
" \(.model): $\(.total_cost)"'
# Total cost for the day
TOTAL_COST=$(an '[.data[].model_breakdown[]?.estimated_cost.amount // 0] | add // 0 | . / 100 | . * 100 | round / 100')
info "Total Claude Code cost for ${REPORT_DATE}: \$${TOTAL_COST}"
pass "7.4 Cost analysis complete"
Validation & Testing
- Run analytics script — verify data returns for active Claude Code users
- Verify per-user session counts, commit counts, and LOC metrics
- Verify tool acceptance rates are calculated correctly
- Confirm cost breakdown by model matches Console dashboard
Expected result: Per-user Claude Code metrics are monitored daily with alerts for anomalies
Monitoring & Maintenance
Ongoing monitoring:
- Daily automated analytics report via cron or CI
- Weekly review of tool acceptance rate trends
- Monthly cost review by user and model
Maintenance schedule:
- Weekly: Review automated analytics reports
- Monthly: Adjust alert thresholds based on team growth
- Quarterly: Full access review correlating Claude Code users with org membership
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.2 | System monitoring |
| NIST 800-53 | AU-6, SI-4 | Audit record review; system monitoring |
| ISO 27001 | A.12.4.1 | Event logging |
5.2 Govern Claude Cowork and Collaborative Sessions
Profile Level: L2 (Walk)
Description
Keep Code collaboration controls separate from Desktop/Cowork administration. Code channels, login pinning, auto mode and transcript retention do not collectively configure the Cowork organization or its native Desktop payload.
Rationale
Why This Matters:
- The earlier shared settings example mixed Code policy with Cowork controls and could imply personal Desktop accounts were prevented.
- Local device files and server-managed Code policy have different effects on Cowork; multiple installed policies can conflict.
Attack Prevented: Shadow-account access and false assurance from applying a policy to the wrong client.
ClickOps Implementation
- Configure Code channels (
channelsEnabled,allowedChannelPlugins), Code organization login (forceLoginMethod,forceLoginOrgUUID) anddisableAutoModeonly in the supported Code managed plane. Disable unneeded remote sessions/routines separately. - Follow Enterprise 3.4 for Cowork/unified enablement, connector approvals, egress and the November 16 rollout. Neither Code channels nor Code auto mode is the Cowork approval toggle.
- Follow Enterprise 3.6 for Desktop org pinning, workspace folders, local MCP and extensions. Code console-delivered settings do not reach Cowork; local Code file policy can, so test both payloads together.
- Use supported transcript cleanup settings and preserve incident evidence first. Device transcripts, Compliance captures and original files have different lifetimes. Do not claim a seven-day Code cleanup deletes cloud conversations or all local artifacts.
Code Implementation
Code Pack: Config
// L2 Walk Code collaboration configuration.
// Disables channels by default, enforces org login,
// restricts session retention, and controls auto-mode.
{
"channelsEnabled": false,
"allowedChannelPlugins": [],
"forceLoginMethod": "claudeai",
"forceLoginOrgUUID": "REPLACE-WITH-YOUR-ORG-UUID",
"cleanupPeriodDays": 7,
"disableAutoMode": "disable",
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
},
"companyAnnouncements": [
"Reminder: All Claude Code sessions are subject to organizational security policies. Report suspected incidents through the organization’s approved security channel."
]
}
// Auto mode governance settings
// Reference: code.claude.com/docs/en/auto-mode
// DANGER: autoMode.allow and autoMode.soft_deny REPLACE the entire
// default rule lists when set — see 7.01 automode-warning section.
// Only autoMode.environment is safe to set without side effects.
// autoMode is NOT read from project settings (.claude/settings.json)
// to prevent repos from injecting their own allow rules.
{
"disableAutoMode": "disable",
"useAutoModeDuringPlan": false,
"disableDeepLinkRegistration": "disable",
"autoMode": {
"environment": [
"Organization: REPLACE-WITH-YOUR-ORG. Primary use: software development",
"Source control: REPLACE-WITH-YOUR-SCM-HOST and all repos under it",
"Trusted internal domains: REPLACE-WITH-YOUR-INTERNAL-DOMAINS"
]
}
}
// L3 Run Code configuration.
// Minimum (1-day) transcript retention, channels disabled, auto-mode disabled,
// org-locked login, and full managed-only lockdown.
// cleanupPeriodDays minimum is 1: a value of 0 fails validation, and an
// invalid key in a managed file is dropped, leaving the 30-day default.
// A managed cleanupPeriodDays also applies to Claude Desktop and Cowork
// transcripts (desktopSessionCleanupPeriodDays is ignored when it is set).
// Reference: code.claude.com/docs/en/settings-reference#cleanupperioddays
{
"channelsEnabled": false,
"allowedChannelPlugins": [],
"forceLoginMethod": "claudeai",
"forceLoginOrgUUID": "REPLACE-WITH-YOUR-ORG-UUID",
"cleanupPeriodDays": 1,
"disableAutoMode": "disable",
"allowManagedPermissionRulesOnly": true,
"allowManagedHooksOnly": true,
"allowManagedMcpServersOnly": true,
"strictKnownMarketplaces": [],
"availableModels": ["sonnet"],
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
},
"companyAnnouncements": [
"Code session cleanup and extension restrictions are managed by organizational policy. Review the effective local and cloud controls before handling sensitive data."
]
}
Validation & Testing
- Read
/statusandclaude doctorfor Code; inspect Desktop device policy independently and test approved/refused accounts. - Verify Cowork, Chrome, built-in browser and scheduled-task restrictions in their own product/admin plane, including permitted local/cloud sessions.
Expected result: The documented boundary is exercised and any unsupported surface or exception is recorded.
Operational Impact
The shared 7.10 pack now labels its settings as Code policy only. Do not deploy it as the Desktop configuration; reducing retention can remove local forensic evidence.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls | 4.1, 6.8 | Secure configuration and role-based access |
| NIST 800-53 | AC-6, CM-6, AU-6 | Least privilege, configuration and evidence |
| SOC 2 | CC6.1, CC7.2 | Access and monitoring |
Sources: code.claude.com · code.claude.com · support.claude.com · trust.anthropic.com (checked 2026-11-05).
5.3 Establish Incident Response for Claude Code and Cowork
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| NIST 800-53 | IR-4, IR-5, IR-8 |
| SOC 2 | CC7.3, CC7.4 |
Description
Establish incident response procedures specific to Claude Code and Cowork security events. Traditional IR playbooks do not cover AI agent-specific scenarios such as prompt injection leading to data exfiltration, MCP server compromise, unattended scheduled task abuse, or Chrome session hijacking. Define detection, containment, evidence collection, and recovery procedures for these novel attack surfaces.
Rationale
Why This Matters:
- AI agent incidents involve unique attack chains not covered by standard IR playbooks (e.g., prompt injection → MCP tool poisoning → credential exfiltration)
- Cowork’s organization availability toggle (Organization settings → Cowork → Enable for your organization) is the fastest containment action; it is organization-wide, although Enterprise plans can scope Cowork access with groups and custom roles
- Forensic evidence for Cowork is stored locally on user machines in the
.claudefolder — it must be collected before session cleanup runs - OpenTelemetry logs in your SIEM and, on Enterprise plans, the Compliance API (which captures Cowork sessions) are the centralized evidence sources; local-session history still has to be collected from the user’s machine
- Without documented IR procedures, response to AI agent incidents will be ad-hoc and slow
Attack Prevented: Prolonged, uncontained AI agent incidents (prompt injection exfiltration, MCP server compromise, hijacked automation) due to ad-hoc response
Attack Scenarios Requiring IR:
- Prompt injection in a document triggers data exfiltration via MCP server or
curlto attacker endpoint - Malicious MCP server installed by user exfiltrates credentials via tool calls
- Chrome automation hijacked to access sensitive internal systems
- Scheduled task compromised to run unauthorized operations for hours unattended
- Supply chain attack via malicious plugin or skill installation
- Personal account bypass via account switching (without tenant restrictions)
Prerequisites
- Existing organizational IR framework
- OpenTelemetry integration with SIEM (Control 5.1/5.2)
- Familiarity with Claude Desktop local storage locations
ClickOps Implementation
Step 1: Document the Emergency Kill-Switch
- The fastest containment action: an Owner or Primary Owner opens Organization settings → Cowork and turns off Enable for your organization (Use Claude Cowork on Team and Enterprise plans)
- This disables Cowork for ALL users in the organization; Run Cowork in the cloud is a separate toggle on the same page, and Claude in Chrome is disabled separately under Organization settings → Claude in Chrome
- Limitation: the toggle is organization-wide — only Enterprise plans can narrow Cowork access to specific teams, through groups and custom roles
- For Claude Code, retain the managed security baseline. Terminate/restrict the affected sessions, client capability, connector or identity through the documented control; deleting managed policy removes protections and is not a containment action.
- Assign specific team members authority to execute the kill-switch without additional approval
Step 2: Define Forensic Collection Procedures
- Primary evidence source: session history in the
.claudefolder on the user’s local machine - Collect BEFORE
cleanupPeriodDaystriggers automatic deletion — after each session starts, Claude Code sweeps away transcripts older than that many days (minimum 1) - Session transcripts are stored as
.jsonlfiles with timestamped entries - Correlate local evidence with OTel logs in your SIEM using
session_idandprompt.idUUID fields - For Enterprise: the Compliance API captures Cowork sessions (via Claude, Claude Desktop, and Claude Mobile) as well as Chat and Code activity, and admins can retrieve local-session content through it — enable the public Compliance API through the Primary Owner and use the scoped access described in the Enterprise guide; do not infer access from an NDA or Trust Center link
Step 3: Build AI Agent IR Scenarios
- Add these scenarios to your IR playbook:
- Prompt injection → exfiltration: Malicious document or web page injects instructions causing data upload to attacker endpoint. Detection: unexpected outbound network calls in OTel
tool_resultevents. Containment: kill-switch + network block. - MCP server compromise: User-installed or compromised MCP server exfiltrates data via tool calls. Detection: unexpected MCP tool invocations in OTel. Containment: remove MCP server from
managed-mcp.json, push update. - Chrome session hijack: Cowork’s Chrome automation directed to access unauthorized internal systems. Detection: unexpected URLs in OTel browser events. Containment: disable Chrome in Cowork.
- Scheduled task abuse: Prompt injection creates a persistent loop accessing data or sending messages. Detection: long-running sessions, off-hours activity in OTel. Containment: user stops task + kill-switch if needed.
- Plugin/skill supply chain: Malicious plugin installed from marketplace executes unauthorized code. Detection: unexpected plugin installation events. Containment: block marketplace, remove plugin, push managed settings update.
- Prompt injection → exfiltration: Malicious document or web page injects instructions causing data upload to attacker endpoint. Detection: unexpected outbound network calls in OTel
Step 4: Conduct Quarterly Tabletop Exercises
- Run tabletop exercises simulating AI agent-specific attacks
- Recommended scenario: prompt injection in a shared document → data exfiltration via MCP server → detection via OTel → containment via kill-switch → forensic collection from user machine
- Include security team, IT ops, and representative Claude Code/Cowork users
- Update IR playbook based on lessons learned
Step 5: Establish Reporting Channels
- Internal: security team escalation path for suspicious Claude behavior (users should know to immediately stop any suspicious task)
- External: report security vulnerabilities to Anthropic via their HackerOne program
- In-app: users can report suspicious behavior with
/feedback - Anthropic publishes its security contact in security.txt — a HackerOne submission form, under the Responsible Disclosure Policy; it lists no security email address
Time to Complete: ~1 hour (playbook creation) + quarterly tabletop exercises
Code Implementation
Code Pack: API Script
# Forensic evidence collector for Claude Code/Cowork incidents.
# Run on the affected user's machine BEFORE cleanupPeriodDays
# triggers automatic transcript deletion.
#
# Usage: ./hth-anthropic-claude-7.11-incident-response.sh [output-dir]
# Output: timestamped archive of Claude session data + summary
OUTPUT_DIR="${1:-./claude-forensics-$(date +%Y%m%d-%H%M%S)}"
CLAUDE_DIR="${HOME}/.claude"
echo "=== Claude Code/Cowork Forensic Collection ==="
echo "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Hostname: $(hostname)"
echo "User: $(whoami)"
echo "Output: ${OUTPUT_DIR}"
echo ""
mkdir -p "${OUTPUT_DIR}"
# Collect session transcripts (.jsonl files)
if [ -d "${CLAUDE_DIR}" ]; then
echo "[1/5] Collecting session transcripts..."
find "${CLAUDE_DIR}" -name "*.jsonl" -type f 2>/dev/null | while read -r f; do
rel="${f#${CLAUDE_DIR}/}"
mkdir -p "${OUTPUT_DIR}/transcripts/$(dirname "${rel}")"
# -p keeps the original timestamps and mode: evidence must not look newer
cp -p "${f}" "${OUTPUT_DIR}/transcripts/${rel}"
done
transcript_count=$(find "${OUTPUT_DIR}/transcripts" -name "*.jsonl" 2>/dev/null | wc -l)
echo " Collected ${transcript_count} transcript files"
else
echo "[1/5] No .claude directory found — skipping transcripts"
fi
# Collect settings files (may contain evidence of tampering)
echo "[2/5] Collecting settings and configuration..."
for settings_file in \
"${CLAUDE_DIR}/settings.json" \
"${CLAUDE_DIR}/settings.local.json" \
"${HOME}/.claude.json"; do
if [ -f "${settings_file}" ]; then
cp -p "${settings_file}" "${OUTPUT_DIR}/$(basename "${settings_file}")"
echo " Collected: ${settings_file}"
fi
done
# Collect project-level settings from CWD if present
if [ -d ".claude" ]; then
echo "[3/5] Collecting project-level .claude/ directory..."
cp -Rp .claude "${OUTPUT_DIR}/project-claude/"
else
echo "[3/5] No project .claude/ directory in CWD"
fi
# Collect MCP server configs
echo "[4/5] Collecting MCP configurations..."
for mcp_file in \
".mcp.json" \
"${CLAUDE_DIR}/mcp.json"; do
if [ -f "${mcp_file}" ]; then
cp -p "${mcp_file}" "${OUTPUT_DIR}/$(basename "${mcp_file}").mcp-config"
echo " Collected: ${mcp_file}"
fi
done
# Collect managed policy (if accessible): settings, MCP policy, and drop-ins
for managed_dir in \
"/Library/Application Support/ClaudeCode" \
"/etc/claude-code"; do
for managed_file in managed-settings.json managed-mcp.json; do
if [ -f "${managed_dir}/${managed_file}" ]; then
cp -p "${managed_dir}/${managed_file}" "${OUTPUT_DIR}/${managed_file}"
echo " Collected managed policy: ${managed_dir}/${managed_file}"
fi
done
if [ -d "${managed_dir}/managed-settings.d" ]; then
cp -Rp "${managed_dir}/managed-settings.d" "${OUTPUT_DIR}/managed-settings.d"
echo " Collected managed drop-ins: ${managed_dir}/managed-settings.d"
fi
done
# Record a SHA-256 for every collected file so later tampering is detectable
echo "[5/5] Hashing evidence and generating collection summary..."
if command -v sha256sum >/dev/null 2>&1; then HASH_CMD=(sha256sum); else HASH_CMD=(shasum -a 256); fi
( cd "${OUTPUT_DIR}" && find . -type f ! -name SHA256SUMS ! -name SUMMARY.txt | LC_ALL=C sort | \
while IFS= read -r f; do "${HASH_CMD[@]}" "${f}"; done ) > "${OUTPUT_DIR}/SHA256SUMS"
# Generate summary
{
echo "Claude Forensic Collection Summary"
echo "=================================="
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Hostname: $(hostname)"
echo "User: $(whoami)"
echo "CWD: $(pwd)"
echo ""
echo "Files Collected:"
find "${OUTPUT_DIR}" -type f | sort | while read -r f; do
size=$(wc -c < "${f}" | tr -d ' ')
echo " ${f} (${size} bytes)"
done
echo ""
echo "Integrity: SHA256SUMS holds a SHA-256 for every collected file"
echo " Verify: (cd ${OUTPUT_DIR} && ${HASH_CMD[*]} -c SHA256SUMS)"
echo ""
echo "Next Steps:"
echo " 1. Correlate session_id and prompt.id UUIDs with OTel logs in SIEM"
echo " 2. Review transcript .jsonl files for suspicious tool_use entries"
echo " 3. Check settings files for unauthorized MCP servers or hooks"
echo " 4. Preserve this archive per your data retention policy"
} > "${OUTPUT_DIR}/SUMMARY.txt"
cat "${OUTPUT_DIR}/SUMMARY.txt"
echo ""
echo "=== Collection Complete ==="
echo "Archive: ${OUTPUT_DIR}"
echo "To compress: tar -czf ${OUTPUT_DIR}.tar.gz ${OUTPUT_DIR}"
Validation & Testing
- Verify kill-switch authority is documented and assigned to specific team members
- Verify forensic collection procedure can successfully extract
.claudesession files from a test machine - Verify OTel logs in SIEM can be correlated with local session data using
session_id - Run a tabletop exercise for at least one AI agent IR scenario
- Verify all team members know how to execute the kill-switch
Expected result: IR playbook includes AI agent scenarios; kill-switch authority assigned; forensic collection tested; quarterly tabletop cadence established
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | None | IR procedures are transparent to users during normal operations |
| System Performance | None | No runtime impact |
| Maintenance Burden | Medium | Quarterly tabletop exercises and playbook updates |
| Rollback Difficulty | N/A | Procedural control, not a technical setting |
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.3, CC7.4 | Incident detection and response; incident recovery |
| NIST 800-53 | IR-4, IR-5, IR-8 | Incident handling; incident monitoring; incident response plan |
| ISO 27001 | A.16.1.1, A.16.1.5 | Information security incident management; response to incidents |
Compliance Quick Reference
Per-control compliance mappings appear inside each control above. For organization-level SOC 2 / NIST / ISO mappings spanning the Anthropic platform, see the Anthropic Common Controls hub.
Appendix A: References
See the Anthropic platform hub references for the shared reference list; key Claude Code sources:
- Claude Code Security
- Claude Code Managed Settings — delivery mechanisms, source precedence, and
/statusverification - Claude Code Settings Reference — every settings key and its scope
- Claude Code Managed MCP
- Claude Code Sandboxing
- Claude Code IAM & Enterprise Auth
- Use Claude Cowork on Team and Enterprise plans — Cowork admin controls, monitoring, and data storage
- Enforce network-level access control with Tenant Restrictions
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-11-05 | 1.2.0 | ai-drafted | Separated Code from Desktop/Cowork policy; corrected permission/sandbox/plugin boundaries and containment; added remote/cloud/routine controls and federal provider-routing guidance with current variables. | Codex |
| 2026-10-04 | 1.1.1 | ai-drafted | validate-hth-guide run against a live Claude Console organization, read-only: 0 of 22 surfaces exercised live, so ai-validated was not added. The §5.1 Console page at platform.claude.com/claude-code loaded only its headings and placeholder rows (no metric or per-user data; the org had no Claude Code activity), and the §5.1 script, run with an Admin API key, returned 0 records for each day tried. The claude.ai Team/Enterprise admin pages need a Team or Enterprise org. Corrections: the claude.ai managed-settings path in §1.1, §2.2, §3.1 and §5.2 is now Organization settings → Claude Code → Managed settings (was “Admin Settings”); the Team/Enterprise analytics URL in §5.1 is claude.ai/analytics/claude-code. |
Claude Code (Opus 5.5) |
| 2026-09-25 | 1.1.0 | ai-drafted | [SECURITY] Fixes from a validate-hth-guide run (22 surfaces; 0 exercised live — the Console was behind a sign-in wall and the claude.ai admin surfaces need a Team or Enterprise org — so ai-validated was not added). §4.2’s workflow pinned two action commits that do not exist and passed inputs claude-code-action@v1 ignores: repinned, with model, tool and turn limits moved into claude_args. The §4.1 rules-file scanner never matched invisible Unicode: fixed, bidi controls added. Validators for §1.1, §1.2 and §2.1 now merge managed-settings.d/ drop-ins, read disableBypassPermissionsMode only under permissions, and exit non-zero on failure; the §5.1 script no longer aborts on macOS, follows pagination, fails on a day with zero records instead of reporting healthy acceptance rates, and keeps the admin key out of process arguments; the §5.3 collector preserves timestamps and writes SHA256SUMS. Guide: settings keys shown under their parent objects, /status and claude doctor verification, HKCU fallback and first-wins source precedence, allowManagedMcpServersOnly, cleanupPeriodDays minimum 1 (0 fails validation), Cowork admin paths, instructions and Compliance API coverage re-sourced from support.claude.com, tenant-restriction availability and header rules, HackerOne and security.txt contacts. |
Claude Code (Opus 5.5) |
| 2026-08-15 | 1.0.2 | ai-drafted | §5.1 corrections from the Admin API currency pass: prerequisites fixed — the gate is an Admin API key on a Claude Console organization (free for every org with Admin API access, both api pay-as-you-go and subscription Pro/Team customer types), not a Team/Enterprise plan; Claude Enterprise (claude.ai) users’ Claude Code activity reports through the separate Claude Enterprise Analytics API. Documented the cloud-provider monitoring blind spot (sessions via Bedrock, Foundry, Vertex AI, or Claude Platform on AWS are invisible to this API — close with OpenTelemetry or provider-side logging), added the detection dimensions (terminal_type, customer_type, user_actor/api_actor), and the operational facts (up to 1-hour data delay, single-day starting_at queries, limit max 1000). Console hosts canonicalized to platform.claude.com. |
Claude Code (Opus 5) |
| 2026-08-08 | 1.0.1 | ai-drafted | Cheat-sheet cell repair: added missing Attack Prevented line(s) to §5.3 (no content-facts changed) | Claude Code (Fable 5) |
| 2026-08-03 | 1.0.0 | ai-drafted | Split out of the monolithic Anthropic Claude guide as part of the multi-product platform restructure; carries the 11 Claude Code controls (formerly section 7) renumbered into five thematic sections. | Claude Code (Sonnet 5) |
Contributing
Found an issue or want to improve this guide? Open an issue or PR on GitHub. Keep all code in Code Packs (no inline code blocks).