say you wrote a cursor hook to catch the scary database commands, like prisma migrate reset or drizzle-kit push --force. you made it answer "ask", because you want a human to look before it runs, not a hard no.
in two of cursor's run modes, that works the way you'd expect. in auto-review mode, it may not. the command can just run with no prompt.
this post covers what was reported, what cursor staff said about it, and what i'd return instead.
what was reported
the report is cursor forum topic 173694, "beforeShellExecution hook returning ask is ignored in Auto-review mode (honoured in Allowlist and Run Everything), 3.21.16", posted 4 oct 2026. the reporter was on cursor 3.21.16 on windows, with a project-level hook that always answers ask:
{"permission":"ask","user_message":"hook asked","agent_message":"hook asked"}
then they asked the agent to run echo test > t.txt in each mode. their summary:
"A project-level beforeShellExecution hook that answers {"permission":"ask"} gets the approval prompt in Allowlist mode and in Run Everything mode ("Hook requested approval: …"). In Auto-review mode the command runs without any prompt. deny is enforced in all three modes."
their table, cleaned up:
| auto-run mode | hook answers ask | hook answers deny |
|---|---|---|
| allowlist | prompt | not tested |
| auto-review | no prompt, command ran (two trials) | blocked |
| run everything | prompt: "Hook requested approval" | not tested |
they also noted that the hook payload doesn't say which auto-run mode is active, "so a hook can't detect Auto-review and compensate."
what cursor staff said
a cursor community support engineer replied in the same thread on 5 oct 2026:
"You're right: in Auto-review, when Auto-review itself decides a command is safe to run, the hook's "ask" answer currently isn't applied, so no approval prompt appears. "deny" is still enforced in every mode, as you saw. I've flagged this to the team."
and for the meantime:
"If you need to stay in Auto-review, have the hook return "deny" for the commands that must never run without review. The agent will see the hook's message and can ask you before retrying."
the other option they gave was to use allowlist or run everything, where an ask always shows the prompt.
the important bit is the qualifier: ask gets dropped when auto-review decides the command is safe. so you can't count on ask in that mode. as of 17 oct the thread doesn't say it's been fixed, but i haven't re-tested on a newer build, so check yours.
one more thing: which message reaches the agent
a different thread, cursor forum topic 174902 (reporter on cursor 3.24.18), got a staff reply on 16 oct 2026 that matters if you write deny messages:
"when a hook denies a command, only user_message reaches the agent right now (agent_message is dropped)"
and:
"Put any instructions meant for the agent in user_message"
so when you deny, put the "here's what to do instead" text in user_message. the hook below writes the same text into both fields, so it's fine either way.
so: deny, with a useful message
for commands that should never run without a person, return deny and say what the agent should do next. the cursor hooks docs also say exit code 2 counts as deny, and that crashes, timeouts and other non-zero exits fail open unless you set failClosed: true. so set it.
here's the hook. it's 27 lines of node with no dependencies, because if you're on prisma or drizzle you already have node.
.cursor/hooks/deny-db-wipes.mjs:
// beforeShellExecution hook: deny commands that can wipe a database.
// cursor sends JSON on stdin and reads JSON from stdout.
let raw = "";
process.stdin.on("data", (c) => (raw += c));
process.stdin.on("end", () => {
let cmd = "";
try {
cmd = String(JSON.parse(raw).command || "");
} catch {
return reply("deny", "the db-wipe hook could not read the hook payload, so it blocked this command. run it yourself if you meant it.");
}
const rules = [
[/\bprisma\s+migrate\s+reset\b/, "prisma migrate reset drops the database"],
[/\bprisma\s+db\s+push\b[^;&|]*\s--(force-reset|accept-data-loss)\b/, "prisma db push with --force-reset or --accept-data-loss can drop data"],
[/\bdrizzle-kit\s+push\b[^;&|]*\s(--force|-f)\b/, "drizzle-kit push --force auto-approves data loss statements"],
];
const hit = rules.find(([re]) => re.test(cmd));
if (!hit) return reply("allow");
reply("deny",
`blocked by the db-wipe hook: ${hit[1]}. do not retry with a different flag or wrapper. ` +
`tell the user the exact command and which database it targets, and let them run it themselves.`);
});
function reply(permission, msg) {
const out = { permission };
if (msg) Object.assign(out, { user_message: msg, agent_message: msg });
process.stdout.write(JSON.stringify(out) + "\n");
}
.cursor/hooks.json:
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": "node .cursor/hooks/deny-db-wipes.mjs",
"timeout": 10,
"failClosed": true,
"matcher": "prisma|drizzle-kit"
}
]
}
}
the matcher means cursor only runs the hook when the command text contains prisma or drizzle-kit. per the docs, beforeShellExecution matchers are tested against the full command string. project hooks run from the project root, which is why the path is .cursor/hooks/....
the drizzle message quotes drizzle-kit's own help text for --force: "Auto-approve all data loss statements. Note: Data loss statements may truncate your tables and data" (checked in drizzle-kit 0.31.11).
test it without cursor
you don't need the agent to check the logic. pipe fake payloads in:
t() {
printf '{"command":%s,"cwd":"","sandbox":false}' "$(node -p 'JSON.stringify(process.argv[1])' "$1")" \
| node .cursor/hooks/deny-db-wipes.mjs
}
t "npx drizzle-kit push --force"
t "npx drizzle-kit push"
here's what i got on node 20 with a batch of commands:
npx prisma migrate reset -> deny
npx prisma migrate reset --force -> deny
pnpm exec prisma migrate reset -f -> deny
npx prisma migrate dev -> allow
npx prisma migrate deploy -> allow
npx prisma db push -> allow
npx prisma db push --accept-data-loss -> deny
bunx prisma db push --force-reset -> deny
npx drizzle-kit push -> allow
npx drizzle-kit push --force -> deny
npx drizzle-kit push -f -> deny
npx drizzle-kit push --strict --force -> deny
npx drizzle-kit generate && npx drizzle-kit migrate -> allow
cd app && npx drizzle-kit push --force -> deny
bash -c 'npx prisma migrate reset --force' -> deny
npm run db:reset -> allow (miss)
echo prisma migrate reset -> deny (false positive)
npx drizzle-kit push --forceful -> allow
not json -> deny (fails closed)
that's a test of the script, not of cursor. i ran these payloads through node on a linux box. i didn't run them through a live cursor agent in each mode, so check the end-to-end behavior on your own build.
the gaps, honestly
-
wrappers get past it.
npm run db:resetdoesn't containprismaordrizzle-kit, so the matcher never even calls the hook. the same goes for a makefile target or a shell script. either add your script names to the matcher and the rules, or keep destructive scripts out ofpackage.json. -
it only sees text. the hook doesn't know which database
DATABASE_URLpoints at.prisma migrate reseton a throwaway local db gets blocked too, and you run it yourself. that's the trade. -
regex isn't a shell parser. quoting, variables (
$CMD),evaland odd spacing can slip through. it also over-blocks things likeecho prisma migrate reset. that's fine for a guard, just know it. - deny is blunter than ask. in allowlist and run everything mode, ask would have given you a nice prompt. with deny you copy the command and run it yourself. the reporter in 173694 asked for the auto-run mode to be added to the payload so a hook could deny only in auto-review. until something like that ships, deny is the answer that holds in every mode.
- the model can still argue with a deny. a deny stops that one command. it doesn't stop the agent trying another route, like raw sql through psql. the message tells it not to, but that's an instruction, not enforcement.
- behavior can change. this was reported on 3.21.16 and acknowledged on 5 oct. retest after updates.
- none of this replaces the real boundary. the agent's shell shouldn't have a production database url in the first place. run prod migrations from ci, give the app a role that doesn't own the tables if you can, and turn on point-in-time restore or scheduled dumps for anything an agent can reach.
checklist
- [ ] hooks that guard destructive commands return deny, not ask
- [ ] instructions for the agent go in
user_message - [ ]
failClosed: trueon the hook - [ ] matcher and rules cover your own npm scripts and wrappers
- [ ] tested with piped json payloads, then once for real in your actual run mode
- [ ] no prod
DATABASE_URLin the agent's environment - [ ] backups or pitr on every database the agent can reach
Written with AI assistance and checked against Cursor forum topics 173694 and 174902 (including the staff replies), the Cursor hooks documentation, and the drizzle-kit 0.31.11 CLI source. The hook was tested with sample JSON payloads, not inside a live Cursor session.
Top comments (0)