Skip to content

Secure coding agents

A coding agent runs shell commands, edits files, fetches URLs, and calls MCP servers with the developer’s credentials. Arcjet checks each of those tool calls against your policies before the agent makes it, from a hook the agent already fires, and records every other lifecycle event so you can see what the agent did.

Arcjet also screens what the developer typed for prompt injection and sensitive information, and records the whole session as activity in the Console.

There is no SDK to install and no code to change. Publish a policy from the Arcjet Console.

┌─────────────┐ hook ┌───────────────┐ allow ┌───────────────┐
│ Coding agent│───────────────▶│ Arcjet policy │────────────▶│ Tool runs │
└─────────────┘ └───────┬───────┘ └───────────────┘
│
│ deny
▼
┌───────────────┐
│ Agent stops │
└───────────────┘

Coding agent events are tracked using HTTP hooks that report events to Arcjet. When a hook fires, Arcjet runs the policies attached to that event and decides whether the agent can proceed. You can enforce a policy or log only, and a change takes effect in real time. Evaluation runs at the edge in over 300 data centers, so the extra latency stays small.

Policies work across every supported coding agent, so you don’t write a separate policy for each one.

If a policy denies an action, the agent is told the rule ID, for example:

Blocked by Arcjet policy: destructive-command.

Choose one or more Execute on options. There are three: Tool call, Prompt, and Model switch. Events that share an Execute on option share inputs. model is filled only on the vendors in Where model is filled.

Execute onEventsInputs the policy can read
Tool callpre-tool-use, permission-requesttool_name, tool_kind, command, command_tokens, paths, domains, destinations, permission_mode, mcp_server, mcp_tool, model
Promptuser-prompt-submit, user-prompt-expansionprompt, model
Model switchpre-model-switch enforces. post-model-switch is recorded and never adjudicated.model

A model rule on Tool call or Prompt does nothing for Claude Code, Copilot, or Muse Code, and that is intended. It is not an INCOMPLETE policy.

The hook URL names the vendor. Use POST /v1/agent-hooks/claude-code, /copilot, /codex, /cursor, or /muse-code, with event as a query parameter.

eventClaude CodeCopilotCodexCursorMuse CodeDenial honoured
pre-tool-usePreToolUsepreToolUsePreToolUsepreToolUse (aliases: beforeShellExecution, beforeMCPExecution, beforeReadFile)PreToolUseAll
permission-requestPermissionRequestpermissionRequestPermissionRequestNo such hookPermissionRequestClaude Code, Copilot, Codex, Muse Code
user-prompt-submitUserPromptSubmituserPromptSubmittedUserPromptSubmitbeforeSubmitPromptUserPromptSubmitClaude Code, Codex, Cursor, Muse Code
user-prompt-expansionUserPromptExpansion––––Claude Code only
pre-model-switchPreModelSwitch––––Claude Code only

Arcjet tracks all these events, but can only enforce policy on the enforceable events in the preceding table.

eventClaude CodeCopilotCodexCursorMuse CodeWhat it carries
post-model-switchPostModelSwitch––––The model the session is now using
post-tool-usePostToolUsepostToolUsePostToolUsepostToolUse (aliases: afterShellExecution, afterMCPExecution, afterFileEdit)PostToolUseWhat the tool returned
post-tool-use-failurePostToolUseFailurepostToolUseFailure–postToolUseFailurePostToolUseFailureWhy it did not
user-prompt-transformed–userPromptTransformed–––The model-facing prompt after a rewrite
stopStopagentStopStopstopStopThe agent’s final message
subagent-startSubagentStartsubagentStartSubagentStartsubagentStartSubagentStartWork delegated to another agent
subagent-stopSubagentStopsubagentStopSubagentStopsubagentStopSubagentStopA subagent’s final message and its name
session-startSessionStartsessionStartSessionStartsessionStartSessionStartWhen an agent started, and from what
session-endSessionEndsessionEndSessionEndsessionEndSessionEndWhen it stopped, and why
notificationNotificationnotification––NotificationPermission prompts and elicitations
permission-deniedPermissionDenied––––What the agent’s own controls stopped
pre-compactPreCompactpreCompactPreCompactpreCompactPreCompactContext discarded mid-session
post-compactPostCompact–––PostCompactAfter context compaction
errorStopFailureerrorOccurred–––Why a turn failed
config-changeConfigChange––––Settings changed, which is a tamper signal
instructions-loadedInstructionsLoaded––––A CLAUDE.md entering context
task-createdTaskCreated––––A task is being created
task-completedTaskCompleted––––A task is marked complete
teammate-idleTeammateIdle––––An agent-team teammate is about to go idle
cwd-changedCwdChanged––––The working directory changed
directory-addedDirectoryAdded––––A directory added mid-session
file-changedFileChanged––––A watched file changed on disk
elicitationElicitation––––An MCP server requested user input
elicitation-resultElicitationResult––––The user responded to an MCP elicitation

A denial uses the vendor’s own hook-output shape. An allow is always {}. Arcjet never answers permissionDecision: "allow" or Cursor’s permission: "allow": those are grants that skip the agent’s own permission flow. Codex can also rewrite the call when an allow carries updatedInput.

EventClaude Code / Codex / Muse CodeCopilotCursor
pre-tool-usehookSpecificOutput.permissionDecision: "deny"Flat permissionDecision and the nested object{ permission: "deny", user_message, agent_message }
permission-requesthookSpecificOutput.decision: {behavior, message}Flat behavior / messageNo such hook
user-prompt-submitTop-level { decision: "block", reason }Output dropped. Can’t enforce{ continue: false, user_message }
user-prompt-expansionTop-level { decision: "block", reason } (Claude Code)No such hookNo such hook
pre-model-switchhookSpecificOutput.permissionDecision: "deny" (Claude Code). Codex, Cursor, and Muse Code do not fire this.––
EventVendorDenial
pre-model-switchClaude CodehookSpecificOutput.hookEventName is PreModelSwitch, permissionDecision is deny, permissionDecisionReason is the rule id. Exit 0 with that JSON.
pre-tool-useCodexhookSpecificOutput.hookEventName is PreToolUse, permissionDecision is deny, permissionDecisionReason is the rule id. The legacy {decision: "block", reason} shape is also accepted by Codex; send the permissionDecision shape only.
permission-requestCodexhookSpecificOutput.decision.behavior is deny, message is the rule id.
user-prompt-submitCodexTop-level {decision: "block", reason}. reason is the rule id.
pre-tool-useMuse CodehookSpecificOutput.hookEventName is PreToolUse, permissionDecision is deny, permissionDecisionReason is the rule id.
permission-requestMuse CodehookSpecificOutput.decision.behavior is deny, message is the rule id.
user-prompt-submitMuse CodeTop-level {decision: "block", reason}. reason is the rule id.
pre-tool-useCursor{permission: "deny", user_message, agent_message}. Both messages are the rule id. ask is not sent: Cursor accepts it on this event and does not enforce it.
user-prompt-submitCursor{continue: false, user_message}. user_message is the rule id. This is beforeSubmitPrompt.

Policy inputs identify the agent as agent_vendor / agent_product: anthropic / claude-code, github / copilot, openai / codex, cursor / cursor, or meta / muse-code.

  • Claude Code and Copilot HTTP hooks fail open on timeout, network error, and non-2xx, except PreModelSwitch: a timeout there blocks the switch, and the default timeout is 30 seconds. The install sets timeout: 30 on that entry. A slow response can block a legitimate switch. A 401 does not block PreToolUse, but a PreModelSwitch timeout does block the switch. Keep those fail-open facts for every Claude Code and Copilot hook other than PreModelSwitch.
  • Codex, Cursor, and Muse Code are not fail-open HTTP. None of them has an HTTP hook type. The wrapper is the fail-closed boundary: it POSTs the raw stdin to the vendor’s URL and prints the response body; on any transport failure, non-2xx, or non-JSON 2xx, it writes the vendor’s denial shape to stdout and exits 2. Exit 2 is what Codex and Muse Code treat as a deny on PreToolUse and UserPromptSubmit. Cursor is configured with failClosed: true, so a crash, timeout, or invalid JSON denies as well. A wrong key denies every Codex, Cursor, and Muse Code prompt and tool call.
  • Cursor has no permission-request hook. A tool-call policy still runs on preToolUse.
  • Copilot can’t honor a prompt denial. It drops hook output on userPromptSubmitted. Arcjet records the decision and marks it as not enforced. Claude Code, Codex, Cursor, and Muse Code honor a prompt denial.
  • Codex hosted tools skip PreToolUse. WebSearch and other hosted tools don’t use Codex’s local function-tool hook path, so a tool-call policy never sees them.
  • A recorded event can never block. No supported agent offers a point where Arcjet could withhold a tool result. A poisoned web page or MCP response is the most common route for an injection into a coding agent, and the hook can’t catch it, because the tool has already run.
  • A hook binds one client, not one person. A repository-level hook can be deleted, a third-party model provider skips the server-managed settings fetch, and a developer calling the API from another tool is outside all of it. Reconcile against OpenTelemetry or Compliance API ingest to find sessions with no hook decisions.
  • Personal accounts don’t reach Arcjet. A personal Claude, ChatGPT, Copilot, or Cursor login on a corporate laptop uses the same product domains as the enterprise tier. Hooks and Compliance don’t see that session. Refuse it at the network or device with account restrictions.

Allowed models is a normal Rego Guard policy. Write the list of model ids in Rego and choose when it runs: Tool call, Prompt, and Model switch. The hook file does not name the policy.

A rule over model does two different things, depending on which agent fired the hook and which Execute on options you selected:

  • On Claude Code it refuses the switch onto a model that is not in the list.
  • On Codex and Cursor it refuses the prompt and the tool call while a model that is not in the list is selected.

Claude Code denies the switch only. Codex and Cursor deny the prompt and the tool call. Copilot and Muse Code are not covered.

AgentBlocks the switchBlocks the next promptBlocks tool callsHow the hook is installed
Claude CodeYes. PreModelSwitch.No. Those payloads have no model.No. Same reason.HTTP entry, same as the other Claude hooks.
CodexNo. There is no switch hook.Yes. UserPromptSubmit carries model and honours decision: "block".Yes. PreToolUse and PermissionRequest carry model and honour a deny.Command wrapper. Codex has command and MCP-tool hooks, not HTTP.
CursorNo. There is no switch hook.Yes, in the IDE. beforeSubmitPrompt carries model / model_id and honours continue: false.Yes. preToolUse carries the same fields and honours permission: "deny".Command wrapper. Cursor hooks are command-only.
Muse CodeNo. There is no switch hook.No. Official hook docs do not publish a model field.No. Same reason.Command wrapper. Muse Code hooks are command-only.
CopilotNo.No.No.Not this policy. Copilot hook payloads have no model field.

A session that opened on a disallowed model and never switches is stopped on Codex and Cursor (the next prompt and every tool call) and is not stopped on Claude Code or Muse Code. Claude Code still needs the vendor’s own org default or banned-model setting for that case.

Cloud agents:

  • Claude Code cloud sessions run the same managed HTTP hooks, so the switch block applies there.
  • Codex managed hooks from requirements.toml, MDM, or cloud config are trusted and cannot be disabled. The wrapper applies wherever those hooks run.
  • Cursor cloud agents run preToolUse and do not run beforeSubmitPrompt. Tool calls are refused; the prompt itself is not, because it was submitted before the VM existed. Enterprise team hooks reach cloud agents. User-level ~/.cursor/hooks.json does not.

Muse Code has no hosted cloud-agent surface. Managed hooks from managed_hooks_path apply to the terminal and muse exec.

auto, default, inherit, and a blank model are not members of any allowlist unless you listed that exact token. On Cursor they are real selector states. On Claude Code, Copilot, and Muse Code the field is absent, which is a different case: an optional input that is absent does not make the binding incomplete. A Cursor session on Auto is denied unless you listed auto. That is a deliberate edit, not a default.

List canonical model ids. Matching is exact, after lowercasing. Not a prefix and not contains. contains "fable" misses claude-fable-5 and hits any id that happens to include those letters.

See the install sections on Secure Claude Code, Secure OpenAI Codex, Secure Cursor, and Secure Muse Code, and the starter on Coding agent policies.

Execute onClaude CodeCodexCursorMuse CodeCopilot
Model switchto_modelevent does not fireevent does not fireevent does not fireevent does not fire
Promptabsentmodel on UserPromptSubmitmodel_id, else model, on beforeSubmitPromptabsentabsent
Tool callabsentmodel on PreToolUse and PermissionRequestmodel_id, else model, on preToolUseabsentabsent

Absent means the translator does not set the input. It does not set it to "". On Cursor a blank result is the token unknown. unknown is not a member of any allowlist unless you listed that exact token. On Muse Code the field is absent – the translator does not invent a model.