Skip to lesson
Exit
Hooks as Enforcement1 / 2

2 min lesson

beforeShellExecution denies rm -rf in-product

Tell someone how to act on this idea: "The job: you can distinguish steering from enforcement and place hooks at the right lifecycle point."

Step 1 of 2

The job: you can distinguish steering from enforcement and place hooks at the right lifecycle point. Start with this check: The gated action is stopped before it runs, not merely discouraged by a Rule. Keep it small enough to check the result yourself instead of taking the summary on trust.

Hooks stop the shell before it runs

0:26 · narrated
Read this demo as text
  1. Ask the agent to run rm -rf on a temp directory. A Rule might say please don't. That is not enough.
  2. Rejected before the shell ran. The deny-rm hook fired. Change that gate in Cursor Settings, Hooks — not by hoping the model remembers.
Watch once: beforeShellExecution denies rm -rf in-product — a Rule steers, a hook enforces.

Practice next: Now do the module workflow: place a deterministic deny on beforeShellExecution and test both the blocked and allowed paths.

Simulated Cursor 3.12 (macOS, light) — beta educational reconstruction, not the real product.

Learn more

Full explanation

Agent hooks, beforeShellExecution

Enforcement hooks — turn a force-push Rule into a gate that actually holds
Agent hooks, beforeShellExecution
SayWe already have a Rule that says never force-push to main, but cloud agent runs don't reliably honor it — one pushed to main last week.
DoName the exact action to gate: a git push that force-updates the protected main branch. A Rule steers the model; this needs a hard stop.
DoPut the check on the beforeShellExecution hook so it inspects the command before the shell runs it. A hook that runs after execution would only see a push that already left.
Type[[ "$command" =~ git[[:space:]]+push ]] && [[ "$command" =~ --force|--force-with-lease|-f ]] && [[ "$command" =~ (^|[[:space:]])main([[:space:]]|$) ]]
DoMatch a push that both targets main and carries a force flag, so ordinary pushes and read-only commands like git status keep working. A blanket block on git push just teaches people to disable the hook.
Type{ "permission": "deny", "user_message": "Force-push to main is blocked. Open a PR instead." }
SeeIt's a fixed pattern match that returns the same deny every time, not a model call, so a given command always produces the same decision and the logic is easy to step through when it misfires.
DoTest both paths, and run one of them as a cloud agent, not just in the editor.
SeeAgent's git push origin main --force is stopped before the shell executes, with the deny message shown in the run; an ordinary git push origin feature-branch in the same run goes through untouched. The gate fires at the command boundary, and each block is recorded in the hook log for review.
Learn more

Optional practice

Practice: beforeShellExecution denies rm -rf in-product

QYou've applied it. What actually proves the work is done, not just a plausible answer?