Guardrails and safety

A rule in text is a request. The agent usually follows it, but it can overlook it. What must never get through has to be guarded by settings or a hook. What an agent never does without permission, how to lock it down in Cursor and Claude Code, where to keep passwords and how to get back when something goes wrong. As of 25 October 2026.

  • Cursor
  • Claude Code
  • Git

🛑 What an agent never does without permission

Everything that can't be undone or that others will see. The agent should do this only on your explicit instruction.

Rewriting history

  • git push --force, git reset --hard, git rebase, git commit --amend.
  • They rewrite or delete saved work, and with a force push on GitHub too.

Throwing work away

  • git clean, git checkout -- ., git restore, rm -rf, Remove-Item -Recurse.
  • Unsaved changes and deleted files can't be brought back, they don't go to the recycle bin.

Things out there

  • A push to the main branch, a website deploy, publishing a package, changes in a database and deleting things in the cloud.
  • Sending an email, a post or a payment through an MCP server or a browser.
  • Others will see it, or it triggers something else.

✋ What counts as an explicit instruction

“Only on my instruction” works only when it's clear what an instruction is. Otherwise the agent likes to assume one.

In the current message

  • An instruction counts when you write it in the current message. Not in the last session, and not “it obviously follows from the context”.
  • Consent from a past session doesn't count today. For a push and a deploy, the agent asks again.

Not “I'll just deploy it”

  • When the agent fixes a bug, it shouldn't deploy it by itself, even if that seems logical. It stops and asks.
  • With forbidden things, being proactive is a mistake, not helpfulness.

Options A, B, C

  • When the agent offers options and you answer with a letter, that's an instruction for what the option says.
  • That's why a push or a deploy has to be written in the option explicitly, not taken for granted. “A, but without the push” rules the push out.
  • For deleting and rewriting history, the agent asks separately, even when they're in the option.

🧱 Three layers of protection

Each one catches something different. All of them together work best.

1. A rule in text

  • In AGENTS.md, CLAUDE.md or User Rules. It explains to the agent why not to do it.
  • According to the Claude Code documentation, rules are guidance, not enforced settings.
  • How to write them: Rules for agents.

2. Permissions and mode

  • When the agent asks and what it may do on its own. Set in the app and in the settings file.
  • It catches the usual form of a command that the agent typically writes.

3. Hook

  • A script that gets every command before it runs and can block it or ask you.
  • A rule can be overlooked, a hook can't. Code decides, not the model.

No layer is bulletproof. The rule Bash(git push *) in Claude Code stops git push origin main, but according to the documentation not git -C . push origin main. A hook checks only what its list thinks of. That's why a backup in git belongs to the guardrails too, see When something goes wrong.

⚙️ Modes and permissions

Based on the Cursor and Claude Code documentation.

Cursor

WhereSettings → Agents → Approvals & Execution

  • Auto-review (recommended): it runs safe things, runs terminal commands in a sandbox and a classifier judges the rest. According to Cursor, this isn't a security boundary.
  • Allowlist: what's on the list runs without asking, for everything else it asks. The sandbox can be turned on.
  • Run Everything: everything without asking. The risk is all yours.
  • The sandbox lets a command read and write only in the project and in temporary folders. Network access by default only to package servers, like github.com and pypi.org. It protects .git/config, .git/hooks, .vscode and .cursorignore.

Claude Code

WhereThe mode switch in the app, rules in .claude/settings.json

  • Manual: it asks before it first runs a command or edits a file. Accept edits: it edits files without asking.
  • Plan: it only reads and plans, it changes nothing. Auto: a classifier checks the actions instead of you.
  • Bypass permissions: it asks about nothing. According to the documentation, only for a container or a virtual machine.
  • The rules deny, ask and allow are evaluated in this order. An allow rule doesn't override a deny.

🪝 A hook: a brake the agent can't get around

One script for Cursor and Claude Code. The agent doesn't see it and can't talk it round. When a command matches the list, the script returns “block” or “ask”.

1. The script .cursor/hooks/brzdy.js CursorClaude Code

It needs Node.js. Adjust the DENY and ASK lists to suit you.

#!/usr/bin/env node
// A brake for an AI agent. Cursor (.cursor/hooks.json) and Claude Code
// (.claude/settings.json) use the same file. Adjust the lists below to suit you.

// Never runs. When you really need it, run the command by hand in the terminal.
const DENY = [
  [/git\s+push\b.*(--force|\s-f\b)/, 'a force push rewrites history on GitHub'],
  [/git\s+reset\s+--hard/, 'reset --hard deletes unsaved changes'],
  [/git\s+rebase\b/, 'rebase rewrites history'],
  [/git\s+commit\b.*--amend/, 'amend rewrites the last commit'],
];

// Goes through only after you confirm it in a dialog.
const ASK = [
  [/git\s+push\b/, 'a push sends the work to GitHub and may trigger a deploy'],
  [/git\s+(clean|restore)\b|git\s+checkout\s+(--|\.)/, 'throws away unsaved changes in files'],
  [/\brm\s+(-\w+\s+)*-\w*r|remove-item\b.*-recurse|\brmdir\b.*\/s/, 'deletes whole folders without the recycle bin'],
  [/\b(npm|pnpm|yarn)\s+publish\b/, 'publishes a package'],
  [/wrangler\s+(pages\s+)?deploy|vercel\s+.*--prod|netlify\s+deploy/, 'deploys a website'],
];

let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => { raw += chunk; });
process.stdin.on('end', () => {
  let input;
  try {
    // Cursor on Windows sends the input with a BOM at the start. Without removing it, JSON.parse fails.
    input = JSON.parse(raw.replace(/^/, ''));
  } catch {
    // The input can't be read: better to stop. Code 2 means “block” in both tools.
    console.error('The brake could not read the command, so it stopped it.');
    process.exit(2);
  }

  // Claude Code sends the command in tool_input.command, Cursor in command.
  const isClaude = Boolean(input.tool_input);
  const command = String((isClaude ? input.tool_input.command : input.command) || '');
  const cmd = command.toLowerCase().replace(/\s+/g, ' ');

  function answer(decision, reason) {
    if (isClaude) {
      console.log(JSON.stringify({
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          permissionDecision: decision,
          permissionDecisionReason: reason,
        },
      }));
    } else {
      console.log(JSON.stringify({ permission: decision, user_message: reason, agent_message: reason }));
    }
    process.exit(0);
  }

  for (const [pattern, why] of DENY) {
    if (pattern.test(cmd)) answer('deny', `Brake: ${why}. The agent won't run it. If you want it, run the command by hand in the terminal.`);
  }
  for (const [pattern, why] of ASK) {
    if (pattern.test(cmd)) answer('ask', `Brake: ${why}. Confirm whether you really want it.`);
  }

  // Claude Code gets an empty answer, so its own prompting still applies.
  // An “allow” answer would skip it.
  if (!isClaude) console.log(JSON.stringify({ permission: 'allow' }));
  process.exit(0);
});
2. Cursor: .cursor/hooks.json Cursor

failClosed means that when the script crashes, the command doesn't run. Without it, Cursor would let it through.

{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      { "command": "node .cursor/hooks/brzdy.js", "failClosed": true, "timeout": 15 }
    ]
  }
}
3. Claude Code: .claude/settings.json Claude Code

Permissions and the hook in one file. onFailure: "block" stops the command when the script crashes and needs Claude Code 2.1.295 or newer. If you already have the file, add just these two parts to it.

{
  "permissions": {
    "deny": [
      "Bash(git reset --hard *)",
      "Bash(git rebase *)",
      "Read(./.env)",
      "Read(./.env.*)"
    ],
    "ask": [
      "Bash(git push *)",
      "Bash(git clean *)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|PowerShell",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.cursor/hooks/brzdy.js"],
            "timeout": 15,
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

Test it

  • In your own terminal in the project root, run:
    echo '{"command":"git push --force"}' | node .cursor/hooks/brzdy.js
  • It should print "permission":"deny". It works in PowerShell and in Git Bash.
  • Then try git status instead of the force push. It should print "permission":"allow".

What to keep in mind

  • Cursor on Windows sends the script text with an invisible BOM character at the start. The script removes it, otherwise it couldn't read the JSON.
  • In Claude Code the script returns nothing for allowed commands. An “allow” answer would skip its own prompting.
  • The hook reads the text of the command. It also stops a command that merely contains the forbidden words, like writing a file through the terminal. The agent should then write the file with a file edit.

🚦 Before a push, after it and long waits

A push often triggers a website deploy and tests. The agent should know what it's sending out and check how it went.

Before a push

  • Have it run the check for what's changing: lint, build or tests. For a website, for example npm run build.
  • Have it list the commits, the folders that will go out and what that triggers (deploy, CI).
  • Push only after that, and only on your instruction.

After a push

  • Have it check the CI result (gh run list) and the deploy at the host.
  • When something fails, the fix takes priority over other work.
  • When it can't see the status, have it ask instead of reporting done.

Long waits

  • Before a longer background task, have it say roughly how long it will take.
  • Every wait has a hard limit, for example 5 minutes for a build. After the limit, have it report the status, whatever it is.
  • When it hits a login or API error, have it stop and say why. It shouldn't quietly retry.
  • Login tokens for some tools expire after about an hour. Before a longer task, have it refresh them.

🔐 Tokens, passwords and keys

  • Don't paste them into a chat. The conversation is saved in your history and, depending on your account settings, may also be used to improve the model.
  • Give the agent a key through a .env file in the project, not in the rules or in a message. Add .env to .gitignore.
  • In Claude Code, block reading .env with the rule Read(./.env) (it's in the template above). In Cursor, hide the file in .cursorignore. Both mainly guard file reads; a command in the terminal may not be stopped.
  • Tokens for MCP servers belong in the settings in your home folder (~/.cursor/mcp.json), not in the project one that goes into git.
  • For development, create a separate key with as few rights as possible and a spending limit. If it leaks, the damage is small.
  • When a key leaks into a chat, a commit or GitHub, revoke it at the service right away and create a new one. Deleting the message or the commit isn't enough.

🔙 When something goes wrong

The best rescue is the one you prepare in advance.

Beforehand: git commit

  • Before you let the agent loose on a bigger task, save the state with a commit. Then you have somewhere to go back to.
  • git status and git diff then show you exactly what the agent changed.
  • Git for beginners is in GitHub for beginners.

Cursor: checkpoint

  • Click Restore Checkpoint on the previous message in the chat. The files go back to the state before it.
  • It only restores files; the messages in the chat stay.
  • Checkpoints live only on your computer and have nothing to do with git. Git is for permanent history.

Claude Code: /rewind

  • /rewind or Esc twice in an empty field. You pick a message and restore the code, the conversation or both.
  • A file changed only by terminal commands (rm, mv) won't be restored. Nor will edits from subagents and other sessions.
  • That's what git is for.

Going back with git

  • One file back to the last commit: git restore file. It throws away the changes in it.
  • A whole commit back: git revert <commit>. It adds a new commit that undoes it, and the history stays.
  • Don't undo what's already on GitHub with a force push. Use revert.

Afterwards: fix the brake

  • When the agent did something it shouldn't have, add a rule or an entry to the hook.
  • Write the date and what happened next to the rule. Then the agent understands why, and so will you in six months.

📋 Prompts to copy

For an agent in Cursor or Claude Code. Replace the yellow parts in square brackets with your own.

Set up guardrails in my project CursorClaude Code

Below the prompt, paste the script and both configurations from the Hook section.

I want a brake for an AI agent in this project. Below are the hook script and the configuration for Cursor and Claude Code.

Steps:
1. Save the script as .cursor/hooks/brzdy.js.
2. Test it before you wire it up: write a small test script that sends it sample inputs for Cursor ({"command": "..."}) and for Claude Code ({"tool_input": {"command": "..."}}), and show me the results. Put the test commands in a file, not straight into the terminal, otherwise the hook would stop them once it's wired up.
3. Wire it into .cursor/hooks.json and .claude/settings.json. If the files exist, don't overwrite anything in them, just add the new parts and show me the diff.
4. Tell me how to test it by hand.
Don't commit anything.

[paste the script and both configurations here]
What changed and how to go back CursorClaude Code

When the agent did something you didn't expect.

Something went wrong: [what you see, for example: a folder disappeared, the website won't start].

Run only commands that change nothing: git status, git diff --stat and git log --oneline -10. Based on them, list for me:
1. which files changed, were added or disappeared since the last commit,
2. whether anything is already on GitHub,
3. the safest way back, step by step. For every command, write what it does and whether it can be undone.

Don't fix anything and don't run anything until I approve the way back.
Find passwords and keys in the project CursorClaude Code
Go through the project and look for passwords, API keys, tokens and credentials in the code, in the config and in files that git tracks. Don't open the .env file, just check that it's in .gitignore.

For every finding, write the file, the line and which service it's for. Mask the value, show at most the first 4 characters.
At the end, write what to move into .env and which keys to revoke at the service because they're already in the git history.
Don't change anything, just list it for me. If you find nothing, say so straight away.