1 min lesson
Test the command boundary
Build one beforeShellExecution guard and prove its deny, allow and failure paths.
Step 1 of 2
Interactive diagram. Step through it with the Next and Previous controls below, or Tab to a region to read its detail.
diagram: flow
One deny is not enough. Check the safe neighbor and broken guard too.
Learn more
Full explanation
.cursor/hooks.json and block-protected-push.mjs
Project hook that blocks a force-push to main
.cursor/hooks.json and block-protected-push.mjs
SayA Rule already tells Agent not to force-push main. Keep that guidance, but add a command check before the shell because the action must be denied even when the instruction is missed. Server-side branch protection remains the final control.
DoGate force flags only when the push targets main. Ordinary status commands and pushes to feature branches must still work.
Type
{
"version": 1,
"hooks": {
"beforeShellExecution": [{
"command": "node .cursor/hooks/block-protected-push.mjs",
"matcher": "git",
"timeout": 5,
"failClosed": true
}]
}
}DoAdd the project hook in .cursor/hooks.json. Project hook commands run from the project root and can run in cloud agents. failClosed stops a matching action if the guard crashes, times out or returns invalid JSON.
Type
stdin: { "command": "git push origin main --force-with-lease", "cwd": "/project" }
stdout: { "permission": "deny", "user_message": "Force-push to main is blocked. Open a PR instead.", "agent_message": "Do not retry with another force flag." }DoHave block-protected-push.mjs read the hook JSON from stdin. Deny only when the command is a force-push to main. Return allow for other matched Git commands.
SeeThe decision is made before the shell starts. Exit code 2 could also block, but the JSON response gives the user and Agent a useful reason.
DoRun parser tests for flag order, --force-with-lease, -f, main refspecs, a feature branch push and git status. In a disposable repository, make a matched hook process exit with an error and confirm that failClosed blocks the command.
SeeThe narrow deny cases fail before execution. Safe neighboring commands remain allowed, and a broken guard does not silently permit the matched action.
DoTry the blocked command and an allowed feature-branch push in the editor, then repeat the blocked case in a cloud agent from the same repository.
SeeThe project hook denies the force-push before execution in both supported runs, shows the reason and leaves branch protection enabled as the remote backstop.
Learn more
Optional practice
Test yourself on Test the command boundary
QWhich result proves the force-push guard is ready to keep?