Skip to content

Secure Claude Code

Claude Code already fires hooks for prompts, tool calls, model switches, and the rest of the session. Arcjet uses those hooks to enforce your policies and record the activity. You don’t change how developers work.

What is Arcjet? Arcjet is the AI agent runtime security platform. Discover the agents running in your organization, enforce policy across every action, prompt, and tool call, and keep the evidence to prove what happened. Detect prompt injection, authorize agent tool calls, redact PII, and block bots and abuse.

Create a free Arcjet account then use the key to authenticate the Arcjet hooks. It is semi-secret, but can be distributed to multiple devices and team members through environment variables or configuration management.

Claude Code has two channels for organization policy. Endpoint-managed settings are deployed to a device by IT. Server-managed settings are fetched from the claude.ai console. Both sit in the same top precedence tier, above every setting a developer can write.

MechanismWhere the JSON goesReachesRemovable by a developer
Managed settings file (endpoint-managed)/etc/claude-code/managed-settings.json (Linux, WSL), /Library/Application Support/ClaudeCode/managed-settings.json (macOS), C:\Program Files\ClaudeCode\managed-settings.json (Windows)That device only: terminal, IDE extensions, the desktop Code tab, Agent SDK sessionsLocal administrator only
MDM or OS policy (endpoint-managed)macOS com.anthropic.claudecode managed preferences; Windows HKLM\SOFTWARE\Policies\ClaudeCode, value SettingsThe same surfaces, redeployable on a scheduleLocal administrator only
Server-managed settingsclaude.ai console, Admin settings > Claude Code > Managed settingsEvery session that authenticates with an eligible credential.Only by switching provider
Repository settings.claude/settings.json, committedSessions in that repository.Yes.

If your developers use Claude Code on the web, read Cloud sessions.

This template is a complete setup. It installs every enforcement point plus the activity hooks Claude Code fires, so Arcjet can record the session. Replace ajkey_REPLACE_ME with your Arcjet key and put the file at the managed settings path for the operating system.

Show managed-settings.json
managed-settings.json
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=session-start",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=user-prompt-submit",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"UserPromptExpansion": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=user-prompt-expansion",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"PreToolUse": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=pre-tool-use",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"PermissionRequest": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=permission-request",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"PermissionDenied": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=permission-denied",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=post-tool-use",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"PostToolUseFailure": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=post-tool-use-failure",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=notification",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"SubagentStart": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=subagent-start",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=subagent-stop",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"TaskCreated": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=task-created",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"TaskCompleted": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=task-completed",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=stop",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"StopFailure": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=error",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"TeammateIdle": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=teammate-idle",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"InstructionsLoaded": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=instructions-loaded",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"ConfigChange": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=config-change",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"CwdChanged": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=cwd-changed",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"DirectoryAdded": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=directory-added",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"FileChanged": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=file-changed",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=pre-compact",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"PostCompact": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=post-compact",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"PreModelSwitch": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=pre-model-switch",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 30
}
]
}
],
"PostModelSwitch": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=post-model-switch",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"Elicitation": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=elicitation",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"ElicitationResult": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=elicitation-result",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "http",
"url": "https://decide.arcjet.com/v1/agent-hooks/claude-code?event=session-end",
"headers": {
"Authorization": "Bearer $ARCJET_KEY",
"X-Arcjet-Principal": "$USER"
},
"allowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"timeout": 5
}
]
}
]
},
"env": {
"ARCJET_KEY": "ajkey_REPLACE_ME"
},
"allowedHttpHookUrls": [
"https://decide.arcjet.com/*"
],
"httpHookAllowedEnvVars": [
"ARCJET_KEY",
"USER"
],
"permissions": {
"disableBypassPermissionsMode": "disable"
}
}

Claude Code runs every matching hook for an event in parallel, so these entries add one request of latency (typically a few tens of milliseconds). An allow is always {}. The hook file does not name a policy.

  • PreToolUse and PermissionRequest run on Tool call. UserPromptSubmit and UserPromptExpansion run on Prompt. PreModelSwitch runs on Model switch. A denial of the switch keeps the current model. Claude Code doesn’t send a model on prompt or tool events, so a model-list policy doesn’t refuse those. A session that opened on a model that isn’t in the list and never switches isn’t stopped. Use Claude Code’s own org default or banned-model setting for that case. For more information about writing the list, see Allowed models.
  • The other entries record the session. StopFailure posts as event=error. FileChanged records a change only if another hook already named the file in a matcher or a watchPaths response. This template has no matcher.
  • The template omits MessageDisplay, PostToolBatch, Setup, WorktreeCreate, and WorktreeRemove. Those fire too often, duplicate PostToolUse, run as command hooks only, or replace Claude Code’s default git worktree behavior.
  • timeout on every entry. Five seconds is a sensible ceiling for a policy decision. PreModelSwitch uses timeout: 30 because a slow response can block a legitimate switch.
  • No matcher. No label. No surface. A matcher on a model id would hide every other switch or tool call from the policy.
  • X-Arcjet-Principal attributes a session to a developer. It’s untrusted, but useful extra metadata. $USER is unset on Windows, so use $USERNAME there and add it to both allowedEnvVars lists.
  • The hook URLs omit surface. Managed settings reach the terminal, IDE extensions, Desktop, and cloud from one file, so a hard-coded cli mislabels most of that traffic. Arcjet records unknown when you omit the parameter. Set it only when the hook file is specific to one surface.

allowedEnvVars allows $ARCJET_KEY to resolve. Define it in one of the following ways:

  • An env block in the same settings file, as in the template. Claude Code applies env to every subprocess it starts, so any command the agent runs can read the key.
  • A literal header value, "Authorization": "Bearer ajkey_…". It never enters the agent’s environment. Prefer this in a root-owned managed settings file.

The last four keys in the template are the lockdown:

{
"allowManagedHooksOnly": true,
"permissions": { "disableBypassPermissionsMode": "disable" },
"allowedHttpHookUrls": ["https://decide.arcjet.com/*"]
}

Hook entries merge across settings levels, so without allowManagedHooksOnly a developer’s own hooks run alongside yours.

disableBypassPermissionsMode keeps the agent’s own permission flow in place. To refuse tool calls when Claude Code is already in auto, bypassPermissions, or dontAsk, also publish Require human approval (coding-agent.no-auto-mode).

A cloud session, on Claude Code on the web or from claude --cloud, runs in a cloud environment on a fresh clone of the repository rather than on the developer’s machine.

A cloud session reads any committed .claude/settings.json and your organization’s server-managed settings.

Every cloud environment sets a network access level, but Arcjet is blocked by default. Give the environment Custom network access and add decide.arcjet.com to Allowed domains, keeping the default list if your sessions install packages.

Exporting a CLAUDE_CODE_USE_* provider variable, such as CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY, or pointing ANTHROPIC_BASE_URL somewhere other than the Anthropic API, makes Claude Code skip the server-managed settings fetch for that session.

Use endpoint-managed settings for configuration that must always be applied, regardless of any CLAUDE_CODE_USE_* environment variables.

  1. Run /status in Claude Code. Setting sources names the selected managed source and Skipped sources what it skipped. A missing line means no source delivered a policy key, and a different source than you expected is the first-wins trap.

  2. Run /hooks to list every configured hook with its source and URL.

  3. Run claude doctor for the server-managed fetch outcome and any dropped entry, or claude --debug-file <path> to record each hook firing and its result.

  4. Open the site’s Activity in the Arcjet Console and confirm the session appears.

If you can’t deploy hooks, Arcjet can connect to the Claude Compliance API to pull activity directly. Alternatively, Claude Code can export its own OpenTelemetry telemetry to Arcjet through managed settings. See Observe Agent Activity for more details. Hooks are the only way to enforce policies.