v2.145.1

Delegate

c3_delegate hands a bounded task (summarize a diff, explain a function, triage a traceback, draft a docstring or test cases) to another model instead of spending the calling agent's own context and rate on it. The default, backend='host', routes to your own provider one tier down: Claude Code delegates to Haiku, Codex and Grok Build step down reasoning effort, Antigravity steps down to a smaller Gemini model. scout=true lets the delegate read the project itself for a lookup; write_paths lets a Claude worker make a change you've already decided and hand back a diff.

v2.145.0 host Β· claude Β· codex Β· gemini Β· grok Β· ollama Β· auto Measured with c3 delegate-eval, not assumed

Overview

c3_delegate is one MCP tool with one Python entry point, cli.tools.delegate.handle_delegate. Every call is scored by c3 delegate-eval against the same suite, so a routing default here is a measured choice, not a guess β€” the CHANGELOG entries for 2.131 through 2.140 are a sequence of measurements, one change at a time (a "delegate remediation" that started from a bad baseline: of 44,038 C3 tool calls across 65 projects, only 19 were delegations, and 10 of the 12 with a logged outcome had errored).

What it is not. c3_delegate is not a subagent. The delegate has no memory of this conversation, no tools unless scout or write_paths is set, and answers only from the context and file_path C3 packs for it (or, in scout/write mode, what it reads itself under Access Guard). For multi-step work that needs the session's full tool set, use an Agent call or the c3-scout / c3-worker subagents (see below).

Signature

c3_delegate(
  task: str, task_type: str = "ask", context: str = "",
  file_path: str = "", backend: str = "host",
  allow_write_delegation: bool = False,
  tier: str = "", model: str = "", scout: bool = False,
  write_paths: str = "",
)
ParameterDefaultMeaning
taskrequiredThe task or question to delegate
task_type"ask"auto, summarize, explain, review, ask, test, diagnose, ping (one live call to check backend/tier/model/wall time/cost), available (CLI version check only, no model call), plus the backend health checks codex_check / gemini_check / grok_check and codex_resume. auto is inferred from the task text (error/traceback words β†’ diagnose, review/audit β†’ review, test words β†’ test, docstring/document β†’ docstring, summarize/tl;dr β†’ summarize, improve/refactor β†’ improve, else explain).
context""The text the delegate answers from
file_path""Comma-separated files C3 reads under Access Guard and packs into the prompt. A denied path raises (surfaced as a blocked response, not an exception β€” since 2.136.0); a masked path refuses, since a delegate answer cannot carry the mask's disclosure; a file over the packing threshold (8k tokens for claude, delegate.file_max_tokens for the others) goes as its structural map instead of full text, so a line-level question on a big file needs an excerpt or scout=true.
backend"host"See Backends
tier"small"See Tiers
model""A model alias or id; overrides the tier on every backend
scoutfalseSee scout=true
write_paths""See write_paths
allow_write_delegationfalseExplicit opt-in for backends that can write outside C3's control (gemini always; grok in write mode; codex_resume) while Access Guard rules are active. Not needed for claude write mode (write_paths), which runs under the guard in-band.

Quick start

1

A bounded question

c3_delegate(
  task='Summarize this diff for a changelog in three bullets',
  context='<diff>', task_type='summarize',
)

Runs on your host's backend, one tier down (Claude Code β†’ Haiku).

2

A lookup you can't name files for

c3_delegate(
  task='Which functions call charge_card, and where?',
  scout=True,
)

The delegate reads the project itself, read-only, under Access Guard.

3

A change you've already decided

c3_delegate(
  task='Rename calc_total to order_total: its '
       'definition and every use. No alias.',
  write_paths='inv/orders.py, inv/report.py, '
              'tests/test_orders.py',
)

A Sonnet worker makes the edits and returns a diff. You review it and run the tests.


Backends

backend='host' (the default since 2.134.0) routes to the provider the calling agent runs on:

Host (svc.ide_name)Maps to backend
claude-codeclaude
codexcodex
grok (Grok Build)grok
antigravitygemini

A host with no mapped backend, or whose mapped backend Access Guard would block, falls back to auto with a note in the response. host runs at delegate.default_tier (small).

Explicit backends

BackendRunsNotes
claude claude -p --safe-mode --strict-mcp-config --tools "", prompt on stdin, in a throwaway directory with Claude Code's nesting variables removed Tool-less since 2.133.0, so it cannot write and an active Access Guard no longer blocks it. scout=true or write_paths add tools back under fences (see below). Usage (tokens, cache, cost, model, turns) lands in telemetry.
codex Codex CLI, sandboxed, reasoning effort per tier Its sandbox is read-only in the project by default β€” the only backend besides a claude scout that passed every lookup case in the eval. codex_default_model no longer pins a spark model that ChatGPT logins reject (2.132.0 measured 25/25 calls failing on the old default); empty uses the CLI's own default.
gemini Gemini CLI, --approval-mode yolo Always write-capable β€” needs allow_write_delegation=true while Access Guard rules are active.
grok Grok Build CLI, headless Read-only by default, in a throwaway temp directory (see Grok Build backend). delegate.grok_allow_write=true switches to --yolo in the project and also needs allow_write_delegation.
ollama A local model, chosen per task_type (DELEGATE_TASKS default models: e.g. gemma3n for summarize/docstring, llama3.2:3b for explain/review/test/diagnose/improve, deepseek-r1:1.5b for ask) Free, local, no cloud login required. The internal default for callers that don't pass backend at all. Never picks an Ollama Cloud tag (a name ending :cloud) unless it is configured by its exact name.
auto Cascades over the other backends Tries the host's own backend first when known. Then, by task_type: a heavy type (review, diagnose, improve, test by default, configurable per backend via *_task_types) prefers the cloud CLIs and degrades: codex β†’ gemini β†’ grok β†’ ollama. A light type stays local-first: ollama β†’ codex β†’ gemini β†’ grok.

An unknown backend name is an error (2.134.0), not a silent fall-through to Ollama. A per-backend runtime circuit breaker (breaker_failure_threshold, default 3; breaker_cooldown_seconds, default 60) skips a backend that just failed repeatedly instead of re-spawning a 90–120s CLI on every call, and raises a notification when it trips.


Tiers, effort & thinking caps

tier is a size, not a model id — a pinned id goes stale, an alias follows the CLI to the current model of that size. Aliases fold to a tier: haiku→small, sonnet→medium, opus→large, parent→default. An explicit model always overrides the tier.

Tierclaudecodex / grokgemini
small (default)haikureasoning effort lowgemini-2.5-flash-lite
mediumsonnetreasoning effort mediumgemini-2.5-flash
largeopusreasoning effort highgemini-2.5-pro
defaultOmit --model / effort flag β€” the CLI's own configured default answers

Codex and Grok keep the account's own model and step reasoning effort instead of the model itself (a pinned model id goes stale β€” 2.132.0 measured the old Codex default rejected outright by ChatGPT logins); delegate.codex_tier_models / delegate.grok_tier_models can still name a model per tier for a project that wants that. delegate.claude_tier_models overrides the claude table.

Which default tier applies

delegate.claude_default_tierA plain delegation (no scout, no write_paths). Falls back to delegate.default_tier, then small.
delegate.claude_scout_default_tierscout=true with no explicit tier. Default medium (2.137.0) β€” a Haiku scout took 28 turns and $0.24 to find one function that Sonnet found in 8 turns for $0.046.
delegate.claude_write_default_tierwrite_paths with no explicit tier. Default medium (2.139.0) β€” see the write-mode measurements below.

Effort and thinking caps (claude only)

Two different knobs, measured separately. --effort had no measurable effect on Haiku 4.5; the thinking-token cap is what actually moves cost.

delegate.claude_effortPasses --effort (low/medium/high/xhigh/max) for every tier. Empty by default (2.137.0): over 22 core eval cases, Haiku with --effort low cost the same as without it, same pass rate, same mean output tokens.
delegate.claude_tier_effortPer-tier override, e.g. {"medium": "low"}. claude_effort wins over this table when both are set.
delegate.claude_thinking_tokensSets MAX_THINKING_TOKENS for every tier; 0 turns thinking off entirely. A delegate never inherits the caller's own thinking budget.
delegate.claude_tier_thinking_tokensPer-tier override. Default {"small": 1024} (2.138.0) β€” only Haiku is capped; Sonnet and Opus run uncapped by default.

Measured (2.138.0) β€” why Haiku is capped at 1024 and Sonnet is not

TargetCore pass (22 cases)Core costp95 wall
Haiku, no cap22/22$0.123321.1s
Haiku, cap 102421/22$0.095717.2s
Haiku, cap 022/22$0.06345.6s
Sonnet, no cap22/22$0.224710.9s
Sonnet, cap 022/22$0.18627.0s

On two real-file reviews, capping Haiku at 1024 cut cost ~55% and wall time 2.6Γ— with no false finding, while cap-0 Haiku produced one false finding and Sonnet at any nonzero cap ignored the cap (9.4k output tokens regardless) β€” Sonnet's depth is the reason it stays uncapped. Config target syntax for comparing caps yourself: c3 delegate-eval --targets claude:small,claude:small+think=0 --targets claude:small+effort=low.


scout=true v2.135.0

A tool-less delegate answers only from what it's given β€” no backend but Codex passed the eval's lookup cases. A scout may read the project itself: on claude, claude -p --restricted --strict-mcp-config --tools Read,Grep,Glob --permission-mode dontAsk --settings …, run in the project; on codex, its own read-only sandbox (already project-scoped).

scout works only on claude and codex (_SCOUT_BACKENDS). host or auto combined with scout=true picks only those two; any other explicit backend with scout=true is an error.

How the fence was built

Measured before shipping it (Claude Code 2.1.270, a .env canary, a repository-wide Grep): with no controls the value leaked; a PreToolUse hook alone still leaked it (a hook sees a Grep's arguments, never its hits); permission denies (Read(**/.env*), Read(./secrets/**)) stopped it; an absolute Read(//C:/…) deny did not; CLAUDE.md is not loaded under --restricted anyway.

  • Every Access Guard deny, confirm-held read, and mask rule becomes a Read() permission deny in the scout's --settings.
  • --restricted ignores every settings file except this one, so the read denies are the only permissions the scout runs with.
  • A corrupt rule scope refuses the scout outright rather than running it unfenced.
  • Scout answers are never cached. Timeout: delegate.claude_scout_timeout (240s).

Measured (2.135.0)

TargetLookup pass (4 cases)Canary leakedMean costp50 / p95 wall
claude:small+scout (Haiku 4.5)4/4no$0.01429.1s / 48.5s
claude:medium+scout (Sonnet 5)4/4no$0.01107.1s / 8.1s

Haiku spent 15 turns and a minute hunting for the denied value before giving up; Sonnet stopped after 2 β€” which is why a scout defaults to medium, not small.


write_paths v2.139.0

The stronger model decides a change and describes it; a cheaper Claude worker makes it and hands back a diff. Claude only, never cascades, defaults to Sonnet. Full design: docs/delegate-write.md.

When to use it

The change is already decided and can be written down: which files, what behavior, the edge cases. Rename across three files, fix a named bug, add a function to a spec, write tests for a given function. Not for open-ended work ("improve this module") or anything that needs commands β€” the worker cannot run tests, so you run them after reading the diff. For multi-step work that needs commands, the c3-worker subagent fits better (see below).

Parameter / configMeaning
write_pathsComma-separated, project-relative globs the worker may edit or create. ** crosses directories, * does not. A bare name is that file at the project root, never a basename match anywhere. Absolute paths, .., .git/ and .c3/ are refused before anything runs. At most 50 entries.
delegate.claude_write_default_tierDefault medium (Sonnet)
delegate.claude_write_timeout600s, but always inside the MCP client's own limit when MCP_TOOL_TIMEOUT is set (105s under Claude Code's 120s) β€” C3's own deadline fires first so an abandoned worker doesn't keep writing.
delegate.claude_write_diff_max_chars24000 β€” the edit ledger keeps every change regardless of what's shown

The worker and its fences

The worker is claude -p --restricted --strict-mcp-config --tools Read,Grep,Glob,Edit,Write --permission-mode dontAsk --settings <json>, in the project directory, with no MCP servers and no user or project settings. Four layers:

  1. Claude Code permission rules. Edit(./<glob>) allows are the write set; Read() / Edit() denies come from every Access Guard deny, read_only, confirm and mask rule. Deny beats allow, so a broad write set cannot reopen a held path.
  2. --restricted. No command-running tools; file tools confined to the project; writes to settings, git and tool-configuration files refused.
  3. The guard hook in worker mode (hook_access_guard.py --worker-state), on every file tool: inside the project; not under .git/ or .c3/; not a credential-vault file; Access Guard with no grants and no filed override requests (a confirm hold is a refusal here β€” a headless worker cannot wait for a human); the write set; and no other agent's file lock. Any hook error denies.
  4. Pre-images. The same hook saves each file's content before its first write in the run. C3 diffs against these afterward, so the answer is right even when the worker's own report is not.

What comes back

[delegate:write] claude medium (sonnet) changed 3 file(s) in 21.4s. Review the diff and run the tests before relying on it.
  M inv/orders.py (+1 -1)
  M inv/report.py (+2 -2)
  M tests/test_orders.py (+2 -2)
Refused (1): Edit inv/money.py

--- worker report ---
<the worker's own summary: what it changed, what it could not do, assumptions>

--- diff ---
<unified diff of every change, capped at delegate.claude_write_diff_max_chars>

Each changed file is logged to the edit ledger (tags c3_delegate, claude, the tier). A worker stopped at its deadline still answers [delegate:write-failed] and lists what it changed. A literal write path Access Guard already refuses (.env, CLAUDE.md) is dropped before the worker starts; if every path is refused, nothing runs ([delegate:blocked]).

Measured (2026-09-14, 66 worker runs) β€” why Sonnet is the default

TargetPassCost/runWall p50Tokens the lead reads
Haiku, thinking cap 102430/33$0.03223.2s836
Sonnet33/33$0.03415.4s582
Haiku, no thinking cap (hard cases)8/10$0.03942.0s1000

Haiku failed where the spec was subtle (a CSV line-number off-by-one twice, a $-0.05 parsed as +5 once). No run, Haiku or Sonnet, touched a file outside its write set or reached .env; both reported the part they couldn't do. Cost is close to a wash; the lead's own reading cost is not, since Sonnet's diffs were smaller.

Telemetry. Every write call's detail carries mode: write, files_changed, lines_added, lines_removed and denied; the [delegate:7d] status line adds N file(s) written.


Delegation without the agent deciding v2.136.0 / v2.140.0

c3_delegate only saves anything when an agent calls it, and agents rarely do (19 of 44,038 calls at the start of this work). Two mechanisms close that gap without needing a decision. Design: docs/delegate-auto.md.

Downshift subagents v2.136.0

c3 install-mcp (Claude Code) writes two subagents to .claude/agents:

SubagentModelToolsFor
c3-scoutHaikuRead, Grep, Glob, and C3's read tools β€” never editsBounded lookups
c3-workerSonnetThe session's own toolsWell-specified changes

They run inside your session, so Access Guard, the edit ledger and discipline hooks apply to them as they do to the parent. Install rewrites only files carrying C3's marker; a file without it is yours and is left alone. Skip both with --no-agents.

Subagent downshift (the hook) v2.140.0

Measured over ~/.claude/projects transcripts since 2026-07-01: 963 of 991 Agent calls had no model, and on Claude Code 2.1.270 such a call runs on the parent's own model β€” a probe with a Sonnet parent put an entire Explore run, and a general-purpose run, on Sonnet. So an Opus or Fable session paid its own rate for every search-and-summarize branch it spawned.

A PreToolUse hook (cli/hook_agent_model.py, policy in services/agent_downshift.py) fills a model-less Agent/Task call's model one tier down, via hookSpecificOutput.updatedInput. Verified live: an Opus parent's model-less Explore call ran on Sonnet.

delegate.agent_downshiftDefault "one_down" (Fable β†’ Opus β†’ Sonnet). "off" disables it. A fixed alias (e.g. "sonnet") applies only when it is below the parent.
delegate.agent_downshift_floorDefault "sonnet" β€” never below this. A Haiku scout took 28 turns where Sonnet took 8 on the eval, and missed subtle write specs Sonnet got right.
delegate.agent_downshift_skipDefault ["fork", "Plan"] β€” a fork ignores model anyway, and planning is where the parent's own strength matters.
delegate.agent_modelsDefault {} β€” {subagent_type: alias}, applied whenever it is below the parent.
C3_AGENT_DOWNSHIFT=0Turns it off for one shell.

Never touched: a call that passes model explicitly; a subagent whose own definition sets model: (like c3-scout/c3-worker); plugin agents; non-Claude hosts. The parent's model is read, in order, from the transcript's last assistant row, then the last model this project saw in the past 14 days, then Claude settings β€” if none is known, nothing changes.

Delegation hints v2.140.0

services/delegate_hints.py watches a per-session window and adds one [c3:delegate-hint] line to a successful c3_edit, c3_read, c3_search or c3_delegate response when the session's own work looks delegable:

KindFires whenSuggests
writeβ‰₯5 c3_edit calls over β‰₯2 files and β‰₯4,000 characters of new text within 15 minutes, or one edit over 6,000 charactersc3_delegate(task=<spec>, write_paths=...)
exploreβ‰₯6 source reads/searches over β‰₯4 files within 10 minutes, with no editc3_delegate(task, scout=True) or an Explore subagent (now downshifted too)

Each kind has a 45-minute cooldown (delegate.hint_cooldown_minutes) and won't fire while the session is already delegating that kind. delegate.hints: false turns them off entirely.

Status line. c3_status(view='budget') shows [delegate-auto:7d] N of M model-less subagent call(s) moved to a lower model, H hint(s) shown, F followed β€” the test of whether any of this changes behavior. If followed stays near zero, the hints are noise.


Telemetry & c3 delegate-eval v2.132.0

Every c3_delegate response writes a detail row to .c3/tool_telemetry.jsonl: host, requested and actual backend, task type, outcome, wall time, and when known tier, model, mode, confidence, cascade, tokens and cost. aggregate_tool_telemetry folds these into delegate_by_backend (calls, outcomes, ok rate, cost, tokens, models, task types, wall p50/p95), which feeds the [delegate:7d] line on c3_status(view='budget'):

# exact format (cli/tools/status.py)
[delegate:7d] {calls} calls, {ok} answered, ${cost} reported{, N file(s) written} ({backend:calls, ...})

What c3 delegate-eval measures

Every case in tests/delegate_eval/gold_suite.jsonl (25 bounded tasks: 22 core, 3 lookup) is sent through handle_delegate against backend[:tier] targets and graded by regex checks (must_match, any_match, must_not_match, max_words), case-insensitive. A no-answer status (error, timeout, blocked, disabled, unavailable, degraded) grades as an error, never as a wrong answer, so a broken backend never reads as a weak model. Core cases carry everything the answer needs in context/file_path; lookup cases carry neither, so only a delegate that reads the fixture project itself (+scout) can pass β€” a live run writes an env canary and lookup-env-secret fails any answer that leaks it. Append +scout to a target to run it that way; write-mode cases live in a separate suite (--suite write, see write_paths).

Running it (verified against c3 delegate-eval --help)

# live: every case costs what the backend costs
c3 delegate-eval --targets claude:small,claude:medium,claude:large --record run.json

# a subset of cases, delegate config overridden just for this run
c3 delegate-eval --targets ollama --cases explain-slice,review-toctou \
  --config '{"allow_model_fallback": false, "preferred_model": "gemma3:12b"}'

# grade a recorded run again β€” free, deterministic
c3 delegate-eval --replay run.json

# CI-style gate: exit 1 when a target's core pass rate is under the floor
c3 delegate-eval --replay run.json --floor 0.8

# --suite: 'gold' (bundled, default), 'write', or a .jsonl path
# --json emits the full report as JSON instead of the table

Live runs work on a throwaway copy of tests/delegate_eval/project, so a delegate that reads or writes never touches the checkout. Pass --allow-write-delegation to measure a write-capable backend on a machine with Access Guard rules. When several tiers of one backend run together, the report names the cheapest passing tier per task type: the lowest mean cost among tiers whose pass rate reaches the floor (default 0.8) and sits within 0.1 of that backend's best tier.

tests/delegate_eval/replay_fixture.json holds a correct and a plausibly-wrong answer for every case; tests/test_delegate_eval.py requires every correct one to pass and every wrong one to fail in CI, so a check that can't fail is caught without spending on a model.


Grok Build backend v2.131.0

xAI's Grok Build CLI (grok) is both a C3 host (c3 install-mcp --ide grok, alias grok-build) and a c3_delegate backend. Full detail: docs/grok-native.md.

c3_delegate(task='...', backend='grok')
# runs: grok --prompt-file <tmp> --output-format json \
#         --tools read_file,grep,list_dir --max-turns N [-m model] --cwd <dir>
ModeRuns whereLoads project MCP/hooks?
Read-only (default)A throwaway temp directory, removed afterward, with only read_file/grep/list_dirNo β€” running outside the project means the project's own .grok config never starts. Answers are cached.
Write (delegate.grok_allow_write=true)The project directory, --yolo auto-approving every callYes β€” needs allow_write_delegation too, the same opt-in as gemini. Never cached.

In both modes the child drops the parent's host session identity and Grok's Claude Code / Cursor imports are disabled, so a delegate never inherits another host's hooks, agents or MCP servers. It also joins the auto cascade (after gemini for heavy task types) and answers task_type='grok_check' with a version-only check (no model call).

delegate.grok_enabledDefault true β€” the grok CLI must also be on PATH
delegate.grok_modelDefault "" β€” empty uses the Grok CLI's own default model
delegate.grok_timeoutDefault 120 (total seconds for one run)
delegate.grok_max_turnsDefault 8
delegate.grok_allow_writeDefault false
delegate.grok_task_typesDefault review, diagnose, improve, test β€” the task types auto routes to Grok

Grok Build uses its own login (e.g. a SuperGrok account); C3 stores no xAI key.


c3_agent β€” a different tool

c3_agent(workflow=..., scope=..., context=...) is not delegation: it orchestrates multiple C3 tool calls inside the calling agent's own turn, for compound investigations that would otherwise be five-plus separate tool calls. Workflows: available, review_changes, prepare_context, investigate, preflight, validate_compress.

# Pre-task check: recall memory + status + recent changes
c3_agent(workflow='preflight', context='starting auth refactor')

# Multi-step: search β†’ compress β†’ summarize β†’ recommend
c3_agent(workflow='investigate', context='why does session restore fail after compact?')

Use c3_delegate to hand work to a different (usually cheaper) model. Use c3_agent to run a known sequence of C3 tools without spelling out each call.


Troubleshooting

SymptomCause & fix
"unknown tier" or "not a Claude model alias or id" tier must be one of small/medium/large/default (or an alias); a model value must look like an alias or id, not a flag.
scout=true refused on a backend other than claude/codex Only claude and codex support scout mode. With backend='host' or 'auto', scout only ever resolves to one of those two.
write_paths refused with [delegate:blocked] Every path you named is refused by Access Guard, a lock, or is outside the project (.git/, .c3/, absolute, ..). Check c3 access list or make the edit yourself.
gemini or grok write mode returns a guard error Pass allow_write_delegation=true β€” these backends can write outside C3's control, so an active Access Guard needs the explicit opt-in. claude write mode (write_paths) does not need this.
A backend that should be up reports errors on every call Its circuit breaker may have tripped after repeated failures (default: 3 failures, 60s cooldown) β€” check the notification C3 raised, or run <cli> --version / task_type='ping' to diagnose directly.
A big file answer feels shallow A file over the packing threshold (8k tokens for claude) travels as its structural map, not full text. Paste the exact excerpt in context, or use scout=true.
No delegation hints ever show up Hints need real volume (5+ edits or 6+ reads in a short window) and a 45-minute cooldown between firings of the same kind; check delegate.hints isn't set to false.

Full design docs: docs/delegate-write.md, docs/delegate-auto.md, docs/delegate-eval.md, docs/grok-native.md, docs/codex-native.md.