Skip to content

Coding agent policies

Arcjet enforces coding agent policies by observing the actions and prompts of coding agents through hooks. Internally they’re built on top of remote agent guard policies, which means you can use Arcjet’s built-in detectors like prompt injection detection and sensitive data protection.

Policies are written in Rego, the policy language used by Arcjet’s guard system. See the Rego documentation for more details.

In the Arcjet Console, go to Policies. The create page asks whether the policy guards a coding agent or an action in your own application.

Choose when the policy should execute: on a Tool call or on a Prompt.

Execute onRunsRules read
Tool callBefore the agent runs a command, reads or writes a file, calls an MCP server, or fetches a URLtool_name, tool_kind, command, command_tokens, paths, domains, mcp_server, mcp_tool
PromptBefore text reaches the model: what the developer typed, and what a slash command expanded intoprompt

Arcjet builds every input from the hook payload. Read a value in Rego as input.values.<name>.

InputKindWhat it holds
contractStringThe contract version, always coding-agent/v1
agent_vendorStringanthropic or github
agent_productStringclaude-code or copilot
agent_surfaceStringThe surface the hook entry declared: cli, ide, cloud, or unknown
eventStringThe lifecycle event, such as pre-tool-use
sessionStringThe agent’s session ID, which also correlates the recorded activity
principalStringThe asserted developer identity from X-Arcjet-Principal. Untrusted
cwdStringThe agent’s working directory
permission_modeStringThe agent’s own permission mode, when it reports one
tool_nameStringThe tool about to be called, verbatim
tool_kindStringshell, file_read, file_write, web, mcp, agent, or other
commandStringThe shell command text, for a shell call
command_tokensString listThe command split on whitespace and the shell’s chaining operators
pathsString listFile paths from the call’s structured arguments
domainsString listHosts of the call’s URL arguments
mcp_serverStringThe MCP server name, for an MCP call
mcp_toolStringThe MCP tool name, for an MCP call
promptStringThe prompt text this event carries

The agents spell their tools differently so Arcjet normalizes them to a consistent tool_kind. Matching is case-insensitive.

tool_kindClaude Code and CopilotCopilot CLI
shellBash, BashOutput, KillShellpowershell
file_readRead, Glob, Grep, NotebookReadview, rg
file_writeEdit, Write, MultiEdit, NotebookEditcreate, str_replace_editor, apply_patch
webWebFetch, WebSearchweb_fetch, web_search
mcpAny mcp__<server>__<tool> nameAny MCP tool
agentAgent, Task
otherEverything else, such as AskUserQuestion and TodoWriteask_user, update_todo

Arcjet will run every policy attached to the specific event in one round trip so you can define multiple policies for the same event. The most restrictive decision will be applied.

Every example assumes the standard preamble:

package arcjet.guard
import rego.v1

A deny over the words in a command. command_tokens is what lets a rule match a word without a regular expression.

destructive := {"rm", "rmdir", "shred", "dd", "mkfs", "sudo", "doas", "trash"}
deny contains "destructive-command" if {
input.values.tool_kind == "shell"
some token in input.values.command_tokens
lower(token) in destructive
}

Two rules over one marker list: one for the files a tool call names, one for the text of a shell command. They’re different inputs because a shell command is opaque text, so a rule that only read paths would miss cat ~/.ssh/id_rsa.

markers := {".ssh/", "id_rsa", "id_ed25519", ".aws/credentials", "/etc/shadow", ".netrc", ".npmrc", ".env"}
deny contains "credential-path" if {
some path in input.values.paths
some marker in markers
contains(path, marker)
}
deny contains "credential-command" if {
input.values.tool_kind == "shell"
some marker in markers
contains(input.values.command, marker)
}

Both rules can fire on one call, such as cp .npmrc /tmp with .npmrc also in paths.

A plain download and a pipe into jq both pass.

deny contains "piped-installer" if {
input.values.tool_kind == "shell"
contains(input.values.command, "|")
some fetcher in input.values.command_tokens
lower(fetcher) in {"curl", "wget"}
some shell in input.values.command_tokens
lower(shell) in {"sh", "bash", "zsh", "dash"}
}

A verb plus a flag.

deny contains "force-push" if {
input.values.tool_kind == "shell"
contains(input.values.command, "git push")
some flag in input.values.command_tokens
flag in {"--force", "-f"}
}
deny contains "hard-reset" if {
input.values.tool_kind == "shell"
contains(input.values.command, "git reset")
"--hard" in input.values.command_tokens
}

The allowlist is written into the policy.

deny contains "unlisted-mcp-server" if {
input.values.tool_kind == "mcp"
not input.values.mcp_server in {"arcjet", "github", "sentry"}
}

A built-in tool isn’t an MCP call, so mcp_server is empty and the rule doesn’t fire.

A deny scoped to writes.

protected := {".github/workflows/", ".git/", "/etc/", "node_modules/"}
deny contains "protected-path-write" if {
input.values.tool_kind == "file_write"
some path in input.values.paths
some marker in protected
contains(path, marker)
}
deny contains "unlisted-domain" if {
input.values.tool_kind == "web"
some host in input.values.domains
not host in {"docs.arcjet.com", "github.com", "raw.githubusercontent.com"}
}

Prompt injection in a developer’s prompt

Section titled “Prompt injection in a developer’s prompt”

Arcjet runs prompt injection detection over the prompt input, so a hook can refuse a turn whose instructions came from somewhere other than the developer: a README, an issue comment, or a web page the agent pasted in.

Declare a server-side prompt injection detector with the ID injection over prompt.

prompt_events := {"user-prompt-submit", "user-prompt-expansion", "user-prompt-transformed"}
deny contains "injected-prompt" if {
input.values.event in prompt_events
input.signals.prompt_injection.injection.detected
}

Sensitive information in a developer’s prompt

Section titled “Sensitive information in a developer’s prompt”

Arcjet runs server-side sensitive information detection over the prompt input, so a hook can refuse a turn that pastes a card number or Social Security number into the prompt.

Declare a server-side sensitive information detector with the ID pii over prompt, denying the CREDIT_CARD_NUMBER and SSN entity types.

prompt_events := {"user-prompt-submit", "user-prompt-expansion", "user-prompt-transformed"}
deny contains "prompt-has-sensitive-info" if {
input.values.event in prompt_events
input.signals.sensitive_info.pii.detected
}
  1. In the Console, go to Policies, choose the coding agent kind, and give the policy a label such as coding-agent.no-npm-publish.

  2. Set what it executes on: Tool call for anything about a command, a file, a URL, or an MCP server, and Prompt for anything about what the developer typed.

  3. Add rules in the visual builder, or start from the closest starter policy and edit its Rego.

  4. Add stored tests. A test input is the contract’s values, so a test for the force-push rule looks like this:

    {
    "values": {
    "tool_kind": "shell",
    "command": "git push --force origin main",
    "command_tokens": ["git", "push", "--force", "origin", "main"]
    }
    }

    Assert the allow as well as the denial.

  5. Publish. Every new rule starts in dry run, so publishing can’t start denying tool calls before you’ve seen it evaluate.

  1. Publish the policy with every rule in dry run.
  2. Let developers work, then open the site’s Activity in the Console and read the tool calls each dry-run rule would have denied.
  3. Set the rule live in a second edit.
  4. Watch the denial reasons developers see. A denial names the rule ID, so a rule ID a developer can understand is one they can work around properly.