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.
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.
Everything that can't be undone or that others will see. The agent should do this only on your explicit instruction.
git push --force, git reset --hard, git rebase, git commit --amend.git clean, git checkout -- ., git restore, rm -rf, Remove-Item -Recurse.“Only on my instruction” works only when it's clear what an instruction is. Otherwise the agent likes to assume one.
Each one catches something different. All of them together work best.
AGENTS.md, CLAUDE.md or User Rules. It explains to the agent why not to do it.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.
Based on the Cursor and Claude Code documentation.
WhereSettings → Agents → Approvals & Execution
.git/config, .git/hooks, .vscode and .cursorignore.WhereThe mode switch in the app, rules in .claude/settings.json
deny, ask and allow are evaluated in this order. An allow rule doesn't override a deny.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”.
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);
});
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 }
]
}
}
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"
}
]
}
]
}
}
echo '{"command":"git push --force"}' | node .cursor/hooks/brzdy.js"permission":"deny". It works in PowerShell and in Git Bash.git status instead of the force push. It should print "permission":"allow".A push often triggers a website deploy and tests. The agent should know what it's sending out and check how it went.
npm run build.gh run list) and the deploy at the host..env file in the project, not in the rules or in a message. Add .env to .gitignore..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.~/.cursor/mcp.json), not in the project one that goes into git.The best rescue is the one you prepare in advance.
git status and git diff then show you exactly what the agent changed./rewind or Esc twice in an empty field. You pick a message and restore the code, the conversation or both.rm, mv) won't be restored. Nor will edits from subagents and other sessions.git restore file. It throws away the changes in it.git revert <commit>. It adds a new commit that undoes it, and the history stays.revert.For an agent in Cursor or Claude Code. Replace the yellow parts in square brackets with your own.
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]
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.
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.