Secure Cursor
Cursor already fires hooks for prompts and tool calls. Arcjet uses those hooks to enforce your policies and record the session. 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.Before you start
Section titled “Before you start”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.
Cursor runs hooks as commands, not as native HTTP handlers. The install is a
small script that reads the hook payload from standard input, posts it to
Arcjet, and writes Arcjet’s response to standard output. The machine that
runs Cursor needs curl.
Choose where to deploy
Section titled “Choose where to deploy”Cursor loads hooks from several sources. When responses conflict, higher-priority sources win: Enterprise, then Team, then Project, then User.
| Mechanism | Where the files go | Reaches | Removable by a developer |
|---|---|---|---|
| Enterprise (MDM) | /Library/Application Support/Cursor/hooks.json (macOS), /etc/cursor/hooks.json (Linux, WSL), C:\ProgramData\Cursor\hooks.json (Windows), plus scripts beside that file | That device: Agent Chat, Cmd+K, and the CLI | Local administrator only |
| Team hooks | Cursor dashboard, Enterprise plan | Members of the team, including cloud agents | Only from the dashboard |
| Project hooks | .cursor/hooks.json, committed | Sessions in that repository, including cloud agents | Yes |
| User hooks | ~/.cursor/hooks.json | That developer’s local IDE and CLI | Yes |
User hooks don’t reach cloud agents. Cloud agent VMs have no access to a developer’s home directory.
Install the hook script
Section titled “Install the hook script”Save this script next to the hook configuration. In a repository, put it at
.cursor/hooks/arcjet-hook.sh. For an enterprise install, put it in the
same directory as the managed hooks.json.
#!/bin/sh# Usage: arcjet-hook.sh VENDOR EVENT# Reads one hook payload from stdin and posts it to Arcjet.# On success: print the body and exit 0.# On any transport failure, non-2xx, or non-JSON 2xx: print the# vendor denial shape and exit 2.
vendor=$1event=$2principal=${USER:-${USERNAME:-}}
deny() { case "${vendor}:${event}" in cursor:pre-tool-use) printf '%s\n' '{"permission":"deny","user_message":"arcjet-hook","agent_message":"arcjet-hook"}' ;; cursor:user-prompt-submit) printf '%s\n' '{"continue":false,"user_message":"arcjet-hook"}' ;; *) printf '%s\n' '{}' ;; esac exit 2}
tmp=$(mktemp) || denytrap 'rm -f "$tmp"' EXITcode=$(curl -sS --max-time 5 -o "$tmp" -w '%{http_code}' -X POST \ "https://decide.arcjet.com/v1/agent-hooks/${vendor}?event=${event}" \ -H "Authorization: Bearer ${ARCJET_KEY}" \ -H "Content-Type: application/json" \ -H "X-Arcjet-Principal: ${principal}" \ --data-binary @-) || denycase "$code" in 2??) ;; *) deny ;;esachead=$(dd if="$tmp" bs=1 count=1 2>/dev/null) || denycase "$head" in \{|\[) ;; *) deny ;;esaccat "$tmp"exit 0Make the script executable. Project hooks run from the project root, so the
command path is .cursor/hooks/arcjet-hook.sh.
On Windows, save a sibling arcjet-hook.ps1:
param([string]$Vendor, [string]$Event)$principal = if ($env:USERNAME) { $env:USERNAME } else { $env:USER }function Deny { switch ("$Vendor`:$Event") { "cursor:pre-tool-use" { Write-Output '{"permission":"deny","user_message":"arcjet-hook","agent_message":"arcjet-hook"}' } "cursor:user-prompt-submit" { Write-Output '{"continue":false,"user_message":"arcjet-hook"}' } default { Write-Output '{}' } } exit 2}try { $body = [Console]::In.ReadToEnd() $headers = @{ Authorization = "Bearer $env:ARCJET_KEY" "Content-Type" = "application/json" "X-Arcjet-Principal" = "$principal" } $response = Invoke-WebRequest -Method Post -TimeoutSec 5 ` -Uri "https://decide.arcjet.com/v1/agent-hooks/$Vendor`?event=$Event" ` -Headers $headers -Body $body if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 300) { Deny } $content = $response.Content if ($content -notmatch '^\s*[\{\[]') { Deny } Write-Output $content} catch { Deny}arcjet-hook reads stdin, POSTs it to
https://decide.arcjet.com/v1/agent-hooks/<vendor>?event=<event> with
Authorization: Bearer $ARCJET_KEY, prints the body, and exits 0. On
failure it prints the denial shape for that vendor and event and exits 2.
Cursor is configured with failClosed: true, so a crash, timeout, or
invalid JSON denies as well. A wrong key denies every Cursor prompt and
tool call. The script does not log the body.
Install the hooks
Section titled “Install the hooks”This template is a complete setup. Put this file in the repository as
.cursor/hooks.json. It installs every enforcement point plus the recorded
events worth keeping. It omits matcher, so Arcjet sees every tool Cursor
reports on preToolUse.
IDE sessions run both preToolUse and beforeSubmitPrompt. Cloud agents
run preToolUse only. 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. A
repository file on the default branch is what cloud agents load.
Cursor has no model-switch hook. A model-list policy refuses the prompt and the tool call while a model that isn’t in the list is selected. For more information about writing the list, see Allowed models.
Show .cursor/hooks.json
{ "version": 1, "hooks": { "preToolUse": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor pre-tool-use", "timeout": 5, "failClosed": true } ], "beforeSubmitPrompt": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor user-prompt-submit", "timeout": 5, "failClosed": true } ], "postToolUse": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor post-tool-use", "timeout": 5 } ], "postToolUseFailure": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor post-tool-use-failure", "timeout": 5 } ], "sessionStart": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor session-start", "timeout": 5 } ], "sessionEnd": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor session-end", "timeout": 5 } ], "subagentStart": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor subagent-start", "timeout": 5 } ], "subagentStop": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor subagent-stop", "timeout": 5 } ], "stop": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor stop", "timeout": 5 } ], "preCompact": [ { "command": "sh .cursor/hooks/arcjet-hook.sh cursor pre-compact", "timeout": 5 } ] }}For an enterprise or user install, change the command to an absolute path
or to ./hooks/arcjet-hook.sh so it resolves from that source’s working
directory.
What the hooks do
Section titled “What the hooks do”Cursor runs every matching hook for an event, so these entries add one request of latency (typically a few tens of milliseconds).
- The wrapper posts to
https://decide.arcjet.com/v1/agent-hooks/cursorwitheventas a query parameter. Policy inputs identify Cursor asagent_vendor: cursorandagent_product: cursor. preToolUseruns on Tool call. The wrapper sendsevent=pre-tool-use. Cursor fires it forShell,Read,Write, MCP,Task, and the other agent tools.beforeSubmitPromptruns on Prompt (event=user-prompt-submit). Cursor honors a prompt denial. Cursor has no permission-request or prompt-expansion hook.- Cursor also has
beforeShellExecution,beforeMCPExecution, andbeforeReadFile. They alias ontopre-tool-useand fire in addition topreToolUse. This template usespreToolUseso one entry covers every tool without posting the same call twice. The matching after hooks (afterShellExecution,afterMCPExecution,afterFileEdit) alias ontopost-tool-use. Don’t install those either. - Cursor reports
Shell,Read,Write,Grep,Delete, andTask. Arcjet maps those toshell,file_read,file_write, andagent. That list is the tools Cursor reports today. A name that isn’t in it istool_kind: other, so a rule over the kind never matches it. A specialized before-hook that omitstool_nameis inferred asShell,Read, orWrite. - Omit
matcher. A matcher that lists onlyShellorWriteleaves reads, MCP tools, and Task calls outside the policy. failClosed: trueon the two enforcement entries. Cursor’s default is fail-open: a crash, a timeout, or invalid JSON lets the action through.timeouton every entry. Five seconds is a sensible ceiling for a policy decision.X-Arcjet-Principalattributes a session to a developer. It’s untrusted, but useful extra metadata.$USERis unset on Windows, so the script falls back to$USERNAME.- The hook URLs omit
surface. The same file reaches the IDE, CLI, and cloud, so a hard-codedidemislabels cloud sessions. Arcjet recordsunknownwhen you omit the parameter. Set it only when the hook file is specific to one surface.
What Arcjet answers
Section titled “What Arcjet answers”The wrapper prints Arcjet’s JSON to standard output. Cursor reads that as the hook result. Echo the body as-is.
| Event | Denial body | Honored |
|---|---|---|
pre-tool-use | { "permission": "deny", "user_message": "RULE_ID", "agent_message": "RULE_ID" } | Yes |
user-prompt-submit | { "continue": false, "user_message": "RULE_ID" } | Yes |
An allow is {}. Cursor treats permission: "allow" as a grant that
skips its own permission flow. The denial reason names the rule IDs that
fired and nothing else.
Supply the Arcjet key
Section titled “Supply the Arcjet key”The script reads ARCJET_KEY from the environment. Export it where Cursor
runs, or inject it through your configuration-management tooling. Never
commit a literal key.
X-Arcjet-Principal uses $USER or $USERNAME. Cursor also sends
user_email on the hook payload when the developer is signed in. Arcjet
reads the asserted principal from the header, not from that field.
Lock it down
Section titled “Lock it down”Enterprise MDM and Team dashboard hooks outrank a developer’s project or
user file. Distribute the script with the hooks.json so the command
path exists on every machine.
Project hooks run only in a trusted workspace. A developer who rejects workspace trust skips the repository file.
Cloud agents
Section titled “Cloud agents”A cloud agent runs command hooks
from the repository. Commit .cursor/hooks.json and
.cursor/hooks/arcjet-hook.sh so the cloud VM can execute them.
On Enterprise plans, cloud agents also run Team hooks and enterprise-managed hooks from the dashboard.
Cloud agents sometimes start in a read-only environment for early exploratory turns. Hooks don’t run during those turns. They start once the agent has a writable environment.
The following hooks don’t run in cloud agents: sessionStart,
sessionEnd, beforeMCPExecution, and afterMCPExecution. Tab hooks
and workspaceOpen are IDE-only. The template still installs
sessionStart and sessionEnd for local sessions.
Cloud agents have internet access by default. If you restrict
network egress,
add decide.arcjet.com to the allowlist for Default + allowlist or
Allowlist only. The hook is an HTTP POST of the vendor payload.
Verify the install
Section titled “Verify the install”-
Open View > Output and select the Hooks channel, or open Customize > Hooks, and confirm the Arcjet commands loaded.
-
Start an agent session in a repository that carries the hook file and ask it to run a harmless command, such as listing a directory.
-
Open the site’s Activity in the Arcjet Console and confirm the session and the tool call appear.
Related
Section titled “Related”- Secure coding agents – the endpoint, the events, and where enforcement stops
- Coding agent policies – the input contract and the starter policies
- Secure Claude Code
- Secure GitHub Copilot
- Secure OpenAI Codex
- Block personal coding agent accounts – keep personal Cursor and other agents off the managed network