Where does the bug appear (feature/product)?
Cursor IDE
Describe the Bug
A subagentStart hook returns permission: “deny” and exits with code 2 for disallowed models, but Cursor still starts or retries the blocked subagent.
The hook log confirms that models such as gpt-5.6-sol-max were denied, yet the subagent continued running.
Screenshot
the hooks log is miss because the limit of record is limited
Steps to Reproduce
- Configure a subagentStart hook with failClosed: true.
- Return permission: “deny” and exit code 2 for a specific model.
- Start a task that creates a subagent using that model.
- Observe that Cursor reports the denial, UI for subagent is
Couldn't Show, but the subagent still running in the background.
Expected Behavior
block the subagent if the model is not allow
Operating System
Windows 10/11
Version Information
Version: 3.12.17 (user setup)
VS Code Extension API: 1.128.0
Commit: 0fb762053c34788bb7760d5673f8a6d4c8589d50
Date: 2026-07-17T02:53:53.006Z
Layout: Agent Window
Build Type: Stable
Release Track: Nightly
Electron: 40.10.3
Chromium: 144.0.7559.236
Node.js: 24.15.0
V8: 14.4.258.32-electron.0
xterm.js: 6.1.0-beta.256
OS: Windows_NT x64 10.0.26200
For AI issues: which model did you use?
Grok 4.5
For AI issues: add Request ID with privacy disabled
Request ID: 906e945e-b976-4147-b57b-411ab67b5c31
fixed after i add rules and prompt to use grok 4.5 explicitly.
Additional Information
I cannot upload the code directly, i paste here:
#~/.cursor/hooks/deny-non-allowlisted-subagent-model.js
#!/usr/bin/env node
// subagentStart / preToolUse(Task) — allow only Composer or Grok 4.5.
// Deny: permission:"deny" + exit code 2. stdout via writeSync (Windows flush).
// https://forum.cursor.com/t/subagent-model-choice-not-respected/163645
'use strict';
const fs = require('fs');
const path = require('path');
const {
decideSubagentStart,
parseHookInput,
} = require('./deny-non-allowlisted-subagent-model-lib.js');
const DEBUG_LOG = path.join(__dirname, 'subagent-model-allowlist.log');
function appendDebug(entry) {
try {
fs.appendFileSync(DEBUG_LOG, `${JSON.stringify(entry)}\n`);
} catch {
// ignore
}
}
function respond(payload, exitCode) {
fs.writeSync(1, JSON.stringify(payload));
process.exit(exitCode);
}
let raw = '';
const stdinTimeout = setTimeout(() => {
respond(
{
permission: 'deny',
user_message:
'Subagent blocked: timed out reading hook payload (model unknown).',
},
2
);
}, 10000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
raw += chunk;
});
process.stdin.on('end', () => {
clearTimeout(stdinTimeout);
try {
const input = parseHookInput(raw);
const decision = decideSubagentStart(input);
appendDebug({
ts: new Date().toISOString(),
hook_event_name: input.hook_event_name,
tool_name: input.tool_name,
subagent_type: input.subagent_type,
subagent_model: input.subagent_model,
model: input.model,
tool_input_model: input.tool_input?.model,
decision: decision.permission,
});
respond(decision, decision.permission === 'deny' ? 2 : 0);
} catch (err) {
appendDebug({
ts: new Date().toISOString(),
error: String(err && err.message ? err.message : err),
raw_preview: String(raw).slice(0, 2000),
decision: 'deny',
});
respond(
{
permission: 'deny',
user_message:
'Subagent blocked: invalid hook payload (could not read model).',
},
2
);
}
});
# ~/.cursor/hooks/deny-non-allowlisted-subagent-model-lib.js
'use strict';
const ALLOW_COMPOSER = /composer/i;
const ALLOW_GROK_45 = /grok[-.]?4\.?5/i;
function isAllowedModel(model) {
if (typeof model !== 'string') return false;
const trimmed = model.trim();
if (!trimmed) return false;
return ALLOW_COMPOSER.test(trimmed) || ALLOW_GROK_45.test(trimmed);
}
function coerceToolInput(toolInput) {
if (typeof toolInput === 'string') {
try {
return JSON.parse(toolInput);
} catch {
return {};
}
}
if (toolInput && typeof toolInput === 'object') return toolInput;
return {};
}
function parseHookInput(raw) {
const text = String(raw ?? '')
.replace(/^\uFEFF/, '')
.trim();
if (!text) return {};
const input = JSON.parse(text);
if (input && typeof input === 'object' && 'tool_input' in input) {
return { ...input, tool_input: coerceToolInput(input.tool_input) };
}
return input;
}
function extractModel(input) {
if (!input || typeof input !== 'object') return '';
const toolInput = coerceToolInput(input.tool_input ?? input.input);
const isTask =
String(input.tool_name || input.tool || '').toLowerCase() === 'task';
// preToolUse(Task): top-level `model` is the PARENT agent — ignore it.
const candidates = isTask
? [toolInput.model, toolInput.subagent_model, input.subagent_model]
: [
input.subagent_model,
toolInput.model,
toolInput.subagent_model,
input.model,
];
for (const value of candidates) {
if (typeof value === 'string' && value.trim()) return value.trim();
}
return '';
}
function decideSubagentStart(input) {
const model = extractModel(input);
// Missing/empty = auto (inherit parent/runtime). Preference order is agent rule:
// Grok 4.5 → Composer → omit.
if (!model) {
return { permission: 'allow' };
}
if (isAllowedModel(model)) {
return { permission: 'allow' };
}
return {
permission: 'deny',
user_message:
`Subagent blocked: model "${model}" is not on the allowlist (composer* or grok-4.5*). ` +
'Retry with Composer or Grok 4.5, omit model for auto, or do the work in the parent agent.',
agent_message:
`Task/subagent denied by allowlist hook. Requested model: ${model}. ` +
'Only composer* or grok-4.5* are allowed (or omit model for auto). Do the work in the parent agent instead of retrying with a blocked model.',
};
}
module.exports = {
isAllowedModel,
extractModel,
parseHookInput,
decideSubagentStart,
};
``
### Does this stop you from using Cursor
No - Cursor works, but with this issue
