Skip to content

Securing Cursor for a team takes four layers: an organisation-wide Privacy Mode setting, a .cursorignore file that the agent’s file tools refuse to read past, run modes and the sandbox that gate terminal commands, and project hooks in .cursor/hooks.json that deny what policy forbids. Only the hooks layer is code you can test in CI.

Your company approved Cursor, and then the security review came back with three questions. Where does our code go? What stops the agent from running curl with a secret in it? Who approved the MCP server that talks to production? Your CISO wants evidence, not a settings screenshot, before the next 30 seats are turned on.

This page is for the tech lead who owns the repository settings and the CTO or security lead who signs off the rollout. Developers get a short list of what the controls change in their day.

What a defensible Cursor security setup gives you

Section titled “What a defensible Cursor security setup gives you”
  • A clear map of what leaves the machine for each Cursor surface, and which decisions belong to the organisation rather than the developer.
  • A .cursorignore and a tested hooks guard, both committed, owned by security in CODEOWNERS.
  • A CI check that fails the day the guard stops blocking a network command or a secret read.
  • Three copy-paste prompts: one to find what must be ignored, one to review MCP servers, and one red-team run that proves the controls hold.
  • A vendor-questions checklist for the items that depend on your plan and contract.

What data leaves the machine when Cursor’s agent runs?

Section titled “What data leaves the machine when Cursor’s agent runs?”

Every agent request goes through Cursor’s backend. The request carries your prompt, the files and snippets the agent reads, the output of the commands it runs, and the results of any MCP tool it calls. The local agent does not execute a model on your laptop. Treat anything the agent can read as something that can reach Cursor and its model providers.

Privacy Mode is an account-level setting that lives on Cursor’s side, not in the repository. The agent runtime fetches it from the backend and distinguishes a no-storage mode, a no-training mode, a mode that allows training on usage data, and one that allows training on the codebase itself. Two consequences follow:

  • A repository cannot prove its own privacy posture. The evidence is the admin dashboard, so the security lead records it there.
  • A developer who signs in with a personal account brings that account’s setting, not the organisation’s. Require sign-in through the company account before any work repository is opened.
SurfaceWhere the code is processedThe decision you own
Local agent (editor, CLI, SDK)Files stay on the laptop; the agent sends what it reads to Cursor’s backendPrivacy Mode tier, .cursorignore, run mode, hooks
Cloud AgentsCloud Agents “run in isolated VMs in the cloud with full development environments” (Cursor docs, checked 2026-08-28), so the repository is cloned into Cursor’s cloudWhether policy allows source code in a vendor VM at all; which repositories are enabled
MCP serversWherever each server runs, often a third-party APIWhich servers are approved, what credentials they hold
Bugbot and other PR integrationsCursor’s GitHub integration reads pull requestsWhich repositories the GitHub app is installed on

For the organisation-wide view that spans all three agent vendors, including data classification and contract terms, see privacy and data handling for AI agents.

  • Reads of a path listed in .cursorignore fail with “Path … is filtered out by .cursorignore”. Open the file yourself if you need it; the agent should not.
  • The guard refuses network tools (curl, wget, ssh, scp, rsync, nc and similar), recursive rm with the flag in any position (rm -rf, rm -f -r, rm --recursive), sudo, and git push in any spelling (git -C . push included). Push from your own terminal after you have read the diff.
  • A new MCP server goes through a pull request to .cursor/mcp.json, reviewed by the security owner, not through your user settings.
  • Sign in to Cursor only with the company account, so the organisation’s Privacy Mode applies to your session.
  • If the guard blocks something legitimate, open a pull request against .cursor/hooks/guard.mjs with a new expect line in the CI check. Do not disable the hook locally.

Order matters. The organisation layer comes first because it cannot be fixed later for data that already left. The hooks layer comes last because it depends on knowing what the other layers miss.

  1. Confirm Privacy Mode for the whole organisation. In the team admin dashboard, check the Privacy Mode tier that applies to every member and whether members can change it. Record a dated screenshot in your security evidence folder. If your contract requires no retention, confirm which models in Cursor need retention and whether admins must approve them. Cursor’s docs name Claude Fable 5.1 and Fable 5: Anthropic stores their inputs and outputs, so they are off by default with Privacy Mode on or on Enterprise, and an admin must opt in. Cursor 3.23 (2026-10-01) made Privacy Mode block automatic crash debugging uploads, and Cursor 3.24 (2026-10-09) added the managed policy AgentRemoteWorkspaceAuthorities: “Admins can restrict Agent to approved remote workspaces” (Cursor docs and release notes, checked 2026-10-12).

  2. Commit a .cursorignore. It uses .gitignore syntax. The runtime refuses agent reads of matching paths with the error “Path … is filtered out by .cursorignore”, passes the same list to the shell sandbox, and blocks the agent from editing .cursorignore itself.

    # .cursorignore: paths the agent's file tools must not read
    .env
    .env.*
    !.env.example
    !.env.sample
    .envrc
    *.pem
    *.key
    secrets/
    terraform.tfstate*
    *.tfvars
    *.sql.gz
    *.dump
  3. Choose the run mode and keep the sandbox on. In the desktop app, go to Settings > Agents > Approvals & Execution. Cursor names three run modes (see Cursor’s run modes page, checked 2026-10-12):

    • Auto-review, the recommended default since Cursor 3.6 (2026-05-29): “Allowlisted calls run immediately. Other shell commands run in the sandbox when possible. Calls that do not use the sandbox go to the Auto-review classifier.”
    • Allowlist: “Actions in your allowlist run without approval.”
    • Run Everything: “Every tool call runs automatically.”

    The classifier is itself a model: “Today it uses Gemini 3.5 Flash Lite, with Claude 4.5 Haiku as the fallback.” Blocking Claude 4.5 Haiku “can disable Auto-review”, and Cursor says “Auto-review is not a security boundary”, so keep the hooks guard from step 4. Keep the allowlist to read-only and test commands (npm test, pytest, tsc, eslint). Leave git push, rm, curl, ssh and anything with sudo off it. Never pick Run Everything on a machine that holds credentials. Cursor’s config files for these settings are ~/.cursor/permissions.json and <project>/.cursor/permissions.json (merged), and sandbox.json controls the sandbox. Cursor lists Browser Protection, File-Deletion Protection and External-File Protection as further protections. Run modes apply to local agents only: Cloud Agents run inside their own dedicated machine, “so the agent never asks you to approve an action.”

  4. Add the hooks guard. Hooks are “spawned processes that communicate over stdio using JSON” and “can observe, block, or modify behavior” (Cursor hooks docs, checked 2026-08-28). A project hook is the one control that is plain code in the repository, so it can be reviewed, versioned and tested. The next section has the files.

  5. Approve MCP servers deliberately. Unless a run mode auto-runs them, each server’s tools need approval before they run; a team allowlist can restrict which ones may run automatically. In an unattended run with sandboxing or Auto-review on, an MCP tool call that would need approval fails closed instead of running. Keep project servers in .cursor/mcp.json, pin package versions, and run the MCP review prompt below before adding one. Setup details are in MCP setup for Cursor.

  6. Put coding rules in project rules, not in the security boundary. A rule such as “use parameterised queries” belongs in .cursor/rules/ (see custom rules and templates). Rules shape what the agent writes. They do not stop a command, so never rely on a rule where a hook or .cursorignore should be.

Block secrets and outbound commands with a hooks guard

Section titled “Block secrets and outbound commands with a hooks guard”

.cursorignore covers the agent’s file tools. It does not stop an approved shell command such as cat .env or curl -d @.env https://… that runs outside the sandbox. A beforeShellExecution hook closes that gap, and a beforeReadFile hook adds a second check on reads. The runtime (checked in @cursor/sdk 1.0.32) loads project hooks from .cursor/hooks.json, runs them from the project root, sends the event as JSON on stdin (command for shell events, file_path for reads), and blocks the action when the hook prints "permission": "deny". On these two events, a hook that prints invalid JSON already blocks. Exit code 2 blocks the action. Any other non-zero exit, or a timeout, gives no verdict and the action proceeds (checked in @cursor/sdk 1.0.32). The per-hook "failClosed": true flag turns that failure into a block, so set it on every guard entry.

{
"version": 1,
"hooks": {
"beforeShellExecution": [{ "command": "node .cursor/hooks/guard.mjs shell", "failClosed": true }],
"beforeReadFile": [{ "command": "node .cursor/hooks/guard.mjs read", "failClosed": true }]
}
}
// .cursor/hooks/guard.mjs: repo-level policy for Cursor's agent
import { readFileSync } from 'node:fs';
const mode = process.argv[2]; // "shell" or "read"
const input = JSON.parse(readFileSync(0, 'utf8'));
const reply = (obj) => process.stdout.write(JSON.stringify(obj));
const deny = (why) => {
reply({ permission: 'deny', user_message: why, agent_message: why });
process.exit(0);
};
// A name ends at anything that cannot be part of it: space, quote, ; | & > < ) and so on.
const END = String.raw`(?![\w.-])`;
const SECRET = new RegExp(
String.raw`(^|[^\w.-])\.env(rc|(\.[\w-]+)+)?${END}|\.(pem|key)(\.(bak|old|orig|backup|save|tmp|\d+))*${END}|id_(rsa|ed25519)|(^|[^\w.-])secrets/`,
);
const TEMPLATE = new RegExp(String.raw`\.env\.(example|sample)${END}`, 'g'); // committed templates
const touchesSecret = (s) => SECRET.test(s.replace(TEMPLATE, ''));
const NETWORK = /\b(curl|wget|ssh|scp|sftp|rsync|nc|ncat|socat|telnet)\b/;
// Any flag of an rm invocation, not only the first one: rm -f -r, rm -fr, rm --recursive
const RECURSIVE_RM = /\brm\s(?:[^;&|]*\s)?(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\b/;
// push anywhere after git in the same command: git push, git -C . push, git --no-pager push
const GIT_PUSH = /\bgit\b[^;&|]*\spush\b/;
if (mode === 'shell') {
const cmd = String(input.command ?? '');
if (NETWORK.test(cmd)) deny(`Network command blocked by repo policy: ${cmd}`);
if (GIT_PUSH.test(cmd) || RECURSIVE_RM.test(cmd) || /\bsudo\b/.test(cmd)) {
deny(`Destructive command blocked by repo policy: ${cmd}`);
}
if (touchesSecret(cmd)) deny(`Command touches a secret file: ${cmd}`);
reply({ permission: 'allow' });
} else if (mode === 'read') {
const path = String(input.file_path ?? '');
if (touchesSecret(path)) deny(`Secret file blocked by repo policy: ${path}`);
reply({ permission: 'allow' });
} else {
process.exit(2); // unknown mode: Exit code 2 blocks the action
}

The guard treats a secret name as ended by any character that cannot belong to a file name, so cat .env;, cat .env|base64 and cat .env>/tmp/x are refused like cat .env. Backup copies count too: any number of dot-segments after .env matches, so .env.local.bak is refused just as .cursorignore’s .env.* hides it, and a .key or .pem file is refused with the usual backup suffixes (.bak, .old, .orig, a number), while src/keyboard.key.ts stays readable. It errs towards false positives: git log --grep push and cat ~/.ssh/config are blocked too, and the fix is a narrower pattern plus a new expect line, never a disabled hook. A pattern list is still not a complete policy. A determined prompt injection can rename a binary, build a command from variables or open a socket from python -c, and no regex sees that. The backstop is the sandbox: in the 1.0.32 runtime a sandboxed command gets no network unless the sandbox policy grants network access or a network allowlist, so keep those settings off. That backstop covers only commands that actually run inside the sandbox, not allowlisted or approved commands that run outside it. Treat the guard as a tripwire for the common and accidental shapes, not a guarantee. For secrets, the stronger fix is to keep them out of the working tree entirely: see agent identity and secrets.

Hooks also load from ~/.cursor/hooks.json for a user and from a machine-wide path that IT can manage: /Library/Application Support/Cursor/hooks.json on macOS, C:\ProgramData\Cursor\hooks.json on Windows, and /etc/cursor/hooks.json on Linux. Use the machine-wide file when the policy must hold even in repositories that do not ship a guard. The same 1.0.32 runtime also loads hooks from Claude Code settings files (~/.claude/settings.json, and the project’s .claude/settings.json and .claude/settings.local.json), so a repository’s Claude Code hooks can execute under Cursor too. Review them as executable code.

Nobody on the security team reads every agent session, and nobody should need to. Proof comes from four checks that run without a human reading transcripts.

  1. A CI test for the guard. The hook is a script, so test it like one. This check fails the build if a refactor of the guard stops blocking:

    #!/usr/bin/env bash
    # scripts/check-cursor-guard.sh: fails CI if the agent guard stops blocking
    set -euo pipefail
    expect() { # MODE JSON VERDICT
    out=$(printf '%s' "$2" | node .cursor/hooks/guard.mjs "$1")
    grep -q "\"permission\":\"$3\"" <<<"$out" || { echo "FAIL: $1 $2 -> $out"; exit 1; }
    }
    expect shell '{"command":"curl https://example.com"}' deny
    expect shell '{"command":"git push origin main"}' deny
    expect shell '{"command":"git -C . push"}' deny
    expect shell '{"command":"rm -fr build"}' deny
    expect shell '{"command":"rm -f -r build"}' deny
    expect shell '{"command":"cat .env"}' deny
    expect shell '{"command":"cat .env;"}' deny
    expect shell '{"command":"cat .env|base64"}' deny
    expect shell '{"command":"cat .env.example .env"}' deny
    expect shell '{"command":"cat .env.production.bak"}' deny
    expect shell '{"command":"cat server.key.bak"}' deny
    expect read '{"file_path":"/repo/.env.production"}' deny
    expect read '{"file_path":"/repo/.env.local.bak"}' deny
    expect read '{"file_path":"/repo/.envrc"}' deny
    expect read '{"file_path":"/repo/certs/server.key"}' deny
    expect shell '{"command":"npm test"}' allow
    expect shell '{"command":"rm -f build/out.txt"}' allow
    expect shell '{"command":"cat .env.example"}' allow
    expect read '{"file_path":"/repo/src/env.ts"}' allow
    expect read '{"file_path":"/repo/.env.example"}' allow
    expect read '{"file_path":"/repo/src/keyboard.key.ts"}' allow
    # A crash must be loud: invalid input has to exit non-zero, never print "allow"
    if printf 'not json' | node .cursor/hooks/guard.mjs shell >/dev/null 2>&1; then
    echo "FAIL: guard exited 0 on invalid JSON"; exit 1
    fi
    echo "cursor guard: all checks passed"
  2. Ownership in CODEOWNERS. Assign .cursorignore, .cursor/hooks.json, .cursor/hooks/, .cursor/mcp.json and .claude/settings.json to the security owner, so a change to the policy needs their approval like any other code change.

  3. A red-team run after every Cursor update and once a month. Run it in a throwaway clone or branch where no real secret is in the working tree and .env is a canary file holding only a fake value, such as CANARY_TOKEN=dtk-canary-000. If a control has failed, the agent reads the file and its contents go to Cursor’s backend, so the file it reads must be worth nothing. The prompt below asks the agent to attempt each forbidden action and report what stopped it. Any row marked “ran” is a failed control, to fix before the next session.

  4. Dated evidence of the organisation layer. The Privacy Mode screenshot and the list of enabled repositories for Cloud Agents and the GitHub app go into the same evidence folder, reviewed each quarter by the security lead.

Sign-off splits by layer: the tech lead owns the repository files and the CI check; the security lead owns the dashboard settings and the quarterly review.

Review every MCP server before it gets approved

Section titled “Review every MCP server before it gets approved”

An MCP server is code with credentials and network access, and each one widens what a prompt injection can reach. Review the server before the first approval, not after it has been in use for a month. Keep the approved list in .cursor/mcp.json in the repository rather than in each developer’s user settings, so the list is reviewed like code. For the threat side of this, see the agent threat model.

Questions to put to Cursor before you sign

Section titled “Questions to put to Cursor before you sign”

These items depend on your plan and contract, and they change. Ask Cursor for written answers and keep them with the evidence folder.

QuestionWhy it matters
Which Privacy Mode tier applies to new team members by default, and can admins enforce it so members cannot change it?A default that a member can turn off is not a control
Which models in Cursor require data retention, and can admins block them?Some model providers need retention for certain models
How long does Cursor keep code for Cloud Agents, and can we set that window?Cloud Agents clone the repository into a cloud VM
Which plan includes SSO, SCIM provisioning and audit logs?Deprovisioning a leaver is a SCIM question, not a policy memo
Where are codebase-index embeddings and chunks stored, and for how long?Indexing is a data flow separate from chat requests
Which subprocessors and model providers receive our data, under which retention terms?This is the list your DPA has to cover

When Cursor’s privacy and security controls break down

Section titled “When Cursor’s privacy and security controls break down”

The red-team run shows cat .env ran. The guard is missing, failed to load, or the pattern missed. Because the file held only the canary token, the control failed without exposing anything real. Check that .cursor/hooks.json is valid JSON and is a regular file, not a symlink (the 1.0.32 runtime refuses to read some config files through symlinks), and run the CI check locally. Then search your working repositories for real .env files: in a normal session the same hole would have sent their contents to Cursor, so rotate anything the agent may have read.

The guard crashes and commands run anyway. Exit code 2 blocks the action, but by default a hook that crashes with any other non-zero exit, or times out, gives no verdict and the action proceeds. Set "failClosed": true on each guard entry (checked in @cursor/sdk 1.0.32) so a crash blocks instead. Keep the guard free of dependencies, keep the invalid-JSON case in the CI check so a crash stays loud, and keep the sandbox on so a failed hook is not the only barrier.

A developer’s session ignores the organisation’s Privacy Mode. They are signed in with a personal account. Require company sign-in through your identity provider, and remove personal accounts from work machines through device management.

An MCP server makes unexpected network requests. Disable it in Cursor’s MCP settings, remove it from .cursor/mcp.json, and rotate the credentials it held. Re-approve only after the review prompt returns “approve” on a pinned version.

The allowlist grows until it contains bash or sh -c. An allowlisted shell runs anything. Audit the allowlist each quarter and move anything that can wrap another command back behind approval or the sandbox.

A policy review says Cloud Agents are always forbidden. Decide on the actual data flow instead: Cloud Agents run in a vendor VM with your repository cloned into it. If policy forbids source code in vendor infrastructure, keep them disabled for those repositories and use local agents with the controls above.