C# / .NETDevOpsMisc
DevOps
Block Cursor Subagents from Using Extra High
Alexandru Puiu
Alexandru Puiu
November 10, 2026
4 min

Table Of Contents

01
What the settings screen does not cover
02
User hook, not a project hook
03
hooks.json
04
The script
05
What each event is allowed to return
06
Check the script before relying on it
07
What still costs money
08
Custom subagents

Cursor can run a subagent on a more expensive effort tier than the chat you selected. Verification, review, and other Task subagents show up as “Extra High” even when the parent conversation is on High or a cheaper model. There is no setting that caps that effort. Disabling the model removes it for you as well.

A user-level hook fixes the gap. When an agent names Extra High on a Task call, the hook rewrites the model to High before the call runs. If Cursor still assigns Extra High when the subagent starts, the hook denies the launch. Your own chat model is left alone.

This is the same cost problem as execution constraints: the expensive work is the work you did not ask for.

What the settings screen does not cover

Three controls look like they should solve this. None of them cap Extra High on a general subagent.

Cursor Settings → Models turns a model on or off. Extra High is an effort variant of a model (xhigh in the slug, “Extra High” in the picker), not a separate product you can leave on while blocking only that tier. Turning the model off removes High too.

Cursor Settings → Agents → Subagents sets the model for the built-in Explore subagent. Review and verification runs are ordinary Task subagents. That dropdown does not apply to them.

A project rule can say “never request Extra High.” The agent may follow it. It can also ignore it and pass claude-opus-5-5-xhigh anyway. Rules are a nudge. The hook runs before the subagent starts, so the request never reaches the model.

A monthly on-demand limit in the Cursor dashboard is still worth setting. It stops every request once the dollar cap is hit. It does not keep a review on High while refusing Extra High.

User hook, not a project hook

Put this on your machine, under ~/.cursor/. A project hook in .cursor/hooks.json belongs in git and applies to everyone who clones the repo. A budget cap is personal.

Cursor loads user hooks from ~/.cursor/hooks.json. Commands in that file run with ~/.cursor/ as the working directory, so the script path is ./hooks/block-extra-high-subagent.py.

Project hooks are different: they live in the repo and the command path is relative to the project root, usually .cursor/hooks/your-script.py.

hooks.json

If you already have a ~/.cursor/hooks.json, add the preToolUse and subagentStart entries and leave the other hooks in place. Cursor uses one file. Replacing it drops whatever else you had configured.

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      {
        "command": "./hooks/block-extra-high-subagent.py",
        "matcher": "Task",
        "type": "command"
      }
    ],
    "subagentStart": [
      {
        "command": "./hooks/block-extra-high-subagent.py",
        "type": "command"
      }
    ]
  }
}

preToolUse fires for every tool. The matcher Task limits this hook to subagent launches. subagentStart fires once Cursor has chosen the model for that subagent, which is the backstop when the tool input never contained a model string.

Matchers are JavaScript regular expressions. Task is exact enough. Do not add a matcher on subagentStart unless you only want to cover one subagent type. Extra High is the problem on every type.

The script

Save this as ~/.cursor/hooks/block-extra-high-subagent.py. It has to be executable, and the shebang line has to use Unix line endings. A CRLF shebang becomes python3\r, and the hook fails with env: python3\r: No such file or directory.

#!/usr/bin/env python3
"""Keep subagents off Extra High effort.

preToolUse on Task rewrites an explicit Extra High model to High.
subagentStart denies the launch if the resolved model is still Extra High.
The parent chat's own model is left unchanged.
"""

import json
import re
import sys

EXTRA_HIGH = re.compile(r"extra[-_ ]?high|\bxhigh\b", re.IGNORECASE)
MODEL_KEYS = ("model", "model_id", "subagent_model")


def is_extra_high(value):
    return isinstance(value, str) and EXTRA_HIGH.search(value) is not None


def to_high(value):
    downgraded = EXTRA_HIGH.sub("high", value)
    return re.sub(r"\s{2,}", " ", downgraded).strip()


def downgrade_model_fields(tool_input):
    if not isinstance(tool_input, dict):
        return tool_input, False

    updated = dict(tool_input)
    changed = False
    for key in MODEL_KEYS:
        current = updated.get(key)
        if is_extra_high(current):
            updated[key] = to_high(current)
            changed = True
    return updated, changed


def allow():
    json.dump({"permission": "allow"}, sys.stdout)
    sys.stdout.write("\n")


def main():
    raw = sys.stdin.read()
    try:
        payload = json.loads(raw) if raw.strip() else {}
    except json.JSONDecodeError:
        allow()
        return

    if not isinstance(payload, dict):
        allow()
        return

    event = payload.get("hook_event_name")

    if event == "preToolUse":
        updated, changed = downgrade_model_fields(payload.get("tool_input"))
        if not changed:
            allow()
            return
        json.dump(
            {"permission": "allow", "updated_input": updated},
            sys.stdout,
        )
        sys.stdout.write("\n")
        return

    if event == "subagentStart" and is_extra_high(payload.get("subagent_model")):
        json.dump(
            {
                "permission": "deny",
                "user_message": (
                    "Blocked an Extra High subagent so it does not spend "
                    "on-demand usage. Rerun it on High or a cheaper model."
                ),
            },
            sys.stdout,
        )
        sys.stdout.write("\n")
        return

    allow()


if __name__ == "__main__":
    main()

Make it executable:

chmod +x ~/.cursor/hooks/block-extra-high-subagent.py

python3 has to be on PATH. Check with command -v python3. On Windows, point the command in hooks.json at python and the script path explicitly if the shebang is not used.

What each event is allowed to return

Command hooks read one JSON object on stdin and write one JSON object on stdout. Exit code 0 means “use that JSON.” Exit code 2 denies the action. Any other non-zero exit fails open unless the hook sets failClosed: true. This script always exits 0 and puts the decision in the JSON, including when the payload is not valid JSON. A crash should not block every subagent. Failing open means a broken script can let Extra High through, which is why the tests below matter.

preToolUse accepts permission, user_message, agent_message, and updated_input. updated_input replaces the tool input. Return the whole input, not just the field you changed, or the prompt and the other arguments disappear.

{
  "permission": "allow",
  "updated_input": {
    "model": "claude-opus-5-5-high",
    "prompt": "review the change"
  }
}

These slugs are rewritten. Plain high is not.

RequestAfter the hook
claude-opus-5-5-xhighclaude-opus-5-5-high
claude-opus-5[effort=extra-high]claude-opus-5[effort=high]
claude-opus-5[effort=extra_high]claude-opus-5[effort=high]
Claude Opus 5.5 Extra HighClaude Opus 5.5 high
claude-sonnet-5-5-highunchanged

The word boundary on xhigh is what keeps high from matching. extra high, extra-high, extra_high, and extrahigh are the display-name forms.

subagentStart only accepts permission and user_message. It cannot rewrite the model. Deny is the only hard stop once Cursor has already picked Extra High.

{
  "permission": "deny",
  "user_message": "Blocked an Extra High subagent so it does not spend on-demand usage. Rerun it on High or a cheaper model."
}

user_message is shown in the client. This event has no agent_message field, so do not add one. The parent agent sees a failed subagent launch and can retry. The retry is cheap only if it does not request Extra High again. The preToolUse rewrite is what makes the first attempt run on High when the agent put the model in the tool input.

The parent conversation’s model is a different field on the hook payload. The script never reads it. Selecting Extra High for your own chat still works.

Check the script before relying on it

Pipe the payloads Cursor documents. You do not need to start an Extra High subagent to see whether the rewrite and the deny work.

hook=~/.cursor/hooks/block-extra-high-subagent.py

printf '%s' '{"hook_event_name":"preToolUse","tool_name":"Task","tool_input":{"model":"claude-opus-5-5-xhigh","prompt":"review"}}' | "$hook"

printf '%s' '{"hook_event_name":"preToolUse","tool_name":"Task","tool_input":{"model":"claude-opus-5[effort=extra-high]","prompt":"review"}}' | "$hook"

printf '%s' '{"hook_event_name":"preToolUse","tool_name":"Task","tool_input":{"model":"claude-sonnet-5-5-high","prompt":"review"}}' | "$hook"

printf '%s' '{"hook_event_name":"subagentStart","subagent_model":"claude-opus-5-5-xhigh"}' | "$hook"

printf '%s' '{"hook_event_name":"subagentStart","subagent_model":"Claude Opus 5.5 Extra High"}' | "$hook"

printf '%s' '{"hook_event_name":"subagentStart","subagent_model":"claude-opus-5-5-high"}' | "$hook"

printf '%s' 'not-json' | "$hook"

Expected results, in order:

  1. permission is allow, and updated_input.model is claude-opus-5-5-high. The prompt is still there.
  2. permission is allow, and the model is claude-opus-5[effort=high].
  3. {"permission": "allow"}. High is not rewritten.
  4. permission is deny for the xhigh slug.
  5. permission is deny for the display name Extra High.
  6. {"permission": "allow"} for a High slug.
  7. {"permission": "allow"} for invalid JSON. The hook fails open.

Cursor reloads hooks.json on save. If a new Extra High subagent card still appears, reload the window. The hook should be listed under Cursor Settings → Hooks, and the Hooks output channel shows whether the script ran.

What still costs money

High is cheaper than Extra High. It is not free. On plans that split Cursor models (Grok, Composer) from other models, a High Opus or Sonnet subagent still draws from the other-models pool, and past that pool it draws on-demand spend. The hook changes the tier. It does not turn verification off.

If a review subagent is the thing burning the budget, also set a fixed on-demand limit in the dashboard. Requests stop at that limit. Usage from a request that is already running when you hit the limit is not billed over the cap unless you raise the limit in that cycle.

To refuse a model entirely, disable it under Cursor Settings → Models. Disabled models drop out of the set a subagent can request. New models tend to ship enabled, so check that list after an update. That is a coarser switch than this hook: you lose the model in the picker too.

Custom subagents

A subagent file under ~/.cursor/agents/ or .cursor/agents/ can pin its own model. model: inherit uses the parent chat. A specific id with an effort parameter pins the tier:

---
name: verifier
description: Validates completed work without raising reasoning effort.
model: claude-opus-5[effort=high]
---

Confirm the change does what was asked. Run the tests that cover it.
Report what passed and what is still incomplete.

That pin applies when Cursor runs that custom subagent. A generic Task call can still name another model. The parent agent is allowed to pass a model when it launches a subagent, which is why the hook has to sit in front of Task and subagentStart rather than only in frontmatter.

Hook input and output fields are documented in Cursor’s hooks reference. Subagent model fields, including inherit and effort, are in the subagents reference.


Tags

ai-agentscursorcost-optimizationhooks
Alexandru Puiu

Alexandru Puiu

Engineer / Security Architect

Systems Engineering advocate, Software Engineer, Security Architect / Researcher, SQL/NoSQL DBA, and Certified Scrum Master with a passion for Distributed Systems, AI and IoT..

Expertise

.NET
RavenDB
Kubernetes

Social Media

githubtwitterwebsite

Related Posts

Agentic Ops Monitor
Building a Local Agentic Ops Monitor - Hourly Log Triage That Opens Issues (and Sometimes PRs)
June 29, 2026
7 min
AI Investigations
C# / .NET
Building an AI Investigations Feature - Root-Cause Analysis Over Your Audit Trail
June 10, 2026
10 min

Subscribe To My Newsletter

I'll only send worthwhile content I think you'll want, less than once a month, and promise to never spam or sell your information!
© 2026, All Rights Reserved.

Quick Links

Get In TouchAbout Me

Social Media