Repository navigation
feat: add --model CLI flag and ASKCC_CLAUDE_MODEL env var to override per-action frontmatter #110
Description
Problem Statement
PR #84 (merged 2026-04-25) delivered the bulk of #80 — subagent-style YAML frontmatter on *_SYSTEM_PROMPT.md templates, declarative per-action tools/model/effort, and runner translation to claude CLI flags. One acceptance criterion from #80 was not delivered:
CLI flags and env vars (
--effort,--model,ASKCC_CLAUDE_EFFORT_LEVEL, etc.) continue to override per-action frontmatter when explicitly set
Today, --effort / ASKCC_CLAUDE_EFFORT_LEVEL and --max-thinking-tokens / ASKCC_CLAUDE_MAX_THINKING_TOKENS follow the documented precedence chain (CLI > env > frontmatter > built-in default), but model has no runtime override path. The only way to change model: opus in DEVELOP_SYSTEM_PROMPT.md is to edit the template file in ~/.askcc/templates/.
Current State
askcc/runners.py:49-60(_frontmatter_cli_flags) emits--modelfromconfig.modelonly — no CLI/env consultation.askcc/cli.pyexposes--effortand--max-thinking-tokensargparse flags but no--model.askcc/settings.pyreadsASKCC_CLAUDE_EFFORT_LEVELandASKCC_CLAUDE_MAX_THINKING_TOKENSbut noASKCC_CLAUDE_MODEL.README.md"Override Precedence" section documents the chain foreffortandmax_thinking_tokens;modelis silently not overridable.VALID_FRONTMATTER_MODELS(askcc/definitions.py:699) already defines the canonical choice set("opus", "sonnet", "haiku", "inherit")— reused for both CLI choices and env validation.
Implementation Plan
Mirror the existing --effort / ASKCC_CLAUDE_EFFORT_LEVEL precedence chain for model. The runner contract stays symmetric: the CLI resolves precedence, then passes the resolved value to runner.run.
-
Add
--modelargparse flag —askcc/cli.pynear the existing--effortargument (askcc/cli.py:139-146). Usechoices=VALID_FRONTMATTER_MODELS(imported fromaskcc/definitions.py:699) so argparse rejects invalid values pre-run. DefaultNone. Help text documents the precedence: CLI > env (ASKCC_CLAUDE_MODEL) > frontmatter > claude's built-in default. -
Add
_resolve_modelhelper — new function inaskcc/cli.pyimmediately after_resolve_max_thinking_tokens(aroundaskcc/cli.py:84), mirroring_resolve_effort(askcc/cli.py:59-71). Signature:_resolve_model(cli_value: str | None, frontmatter_value: str | None) -> str | None. Precedence:- CLI value if non-None
- else env
ASKCC_CLAUDE_MODELif non-empty and inVALID_FRONTMATTER_MODELS(warn-and-ignore on invalid, matching_resolve_effort's warning shape) - else
frontmatter_value(may itself beNone) - Return
Nonewhen no source is set — caller suppresses the--modelflag.
-
Wire resolved value into runner call — in
askcc/cli.py main()nearaskcc/cli.py:286-287whereeffort_level/max_thinking_tokensare resolved, addmodel = _resolve_model(args.model, config.model)and passmodel=modelinto therunner.run(...)call ataskcc/cli.py:290-299. -
Extend
Runner.runsignature —askcc/runners.py:33-46: addmodel: str | None = Nonekeyword-only parameter to the abstract method and toClaudeRunner.run(askcc/runners.py:159-170). InsideClaudeRunner.run, forward it to_frontmatter_cli_flagsso the resolved value (notconfig.model) drives the emitted flag. -
Update
_frontmatter_cli_flags—askcc/runners.py:49-60: accept an optionalmodel: str | None = Noneparameter. Replace theconfig.modellookup witheffective_model = model if model is not None else config.model. Emit["--model", effective_model]only when truthy — preserves "no flag when unset" behavior for templates withoutmodel:. -
Document in README —
README.md:- "Override Precedence" section (lines 236-244): generalize so it covers
effort,max_thinking_tokens, andmodel. - Global options table (lines 110-122): add a
--modelrow. - Environment Variables table (lines 134-148): add an
ASKCC_CLAUDE_MODELrow.
- "Override Precedence" section (lines 236-244): generalize so it covers
-
Tests —
tests/test_askcc.py:TestModelPrecedence(new class) mirroringTestEffortPrecedence(tests/test_askcc.py:2598-2648): four tests covering CLI > env > frontmatter > unset (no default;Nonepropagates).TestThinkingSettingsextensions (tests/test_askcc.py:1827):test_invalid_askcc_claude_model_logged_and_ignoredmirroringtest_invalid_effort_level_logged_and_ignored(tests/test_askcc.py:1846-1853), but exercising_resolve_model(env lookup lives incli.py, notsettings.py— adjust import and caplog logger accordingly).TestRunnerFrontmatterFlagsextensions (tests/test_askcc.py:2651-2702):test_model_override_emitted(runner.run withmodel="sonnet"overridesconfig.model="opus") andtest_model_falls_back_to_config_when_unset(nomodelkwarg →config.modelstill drives the flag).- CLI-level forwarding test following the pattern at
tests/test_askcc.py:2030-2044: assert--model sonnetandASKCC_CLAUDE_MODEL=sonnetboth reachrunner.run(model="sonnet"). - Argparse rejection test: invoking
main()with--model opuzraisesSystemExit(argparse exits 2 on bad choice).
Verification
After implementation, the following must pass:
uv run pytest # all tests pass, including the new TestModelPrecedence class
uv run ruff check # no lint regressions
uv run pyright # no type regressionsManual smoke (against a real issue, optional but recommended before opening the PR):
askcc --model sonnet plan -g <url> # spawns claude with --model sonnet regardless of frontmatter
ASKCC_CLAUDE_MODEL=haiku askcc plan -g <url> # spawns claude with --model haiku when no --model flag
ASKCC_CLAUDE_MODEL=opuz askcc plan -g <url> # WARNING logged, falls through to frontmatter
askcc --model opuz plan -g <url> # argparse exits 2: "invalid choice"Acceptance Criteria
-
askcc --model sonnet develop <url>overridesmodel: opusinDEVELOP_SYSTEM_PROMPT.mdfrontmatter -
ASKCC_CLAUDE_MODEL=sonnet askcc develop <url>does the same when no--modelflag is set - Invalid values:
--model opuzrejected by argparse;ASKCC_CLAUDE_MODEL=opuzlogs a warning and falls through to frontmatter/default -
README.md"Override Precedence" section extended to covermodelalongsideeffortandmax_thinking_tokens -
README.mdEnvironment Variables table listsASKCC_CLAUDE_MODEL; global options table lists--model - Tests cover: CLI > env > frontmatter > unset precedence; invalid env warn-and-ignore; runner emits/omits
--modelcorrectly; argparse rejects invalid CLI choice -
uv run pytest,uv run ruff check,uv run pyrightall pass
Dependencies
None identified — self-contained additive change inside askcc/cli.py, askcc/runners.py, README.md, and tests/test_askcc.py. No new package dependencies. VALID_FRONTMATTER_MODELS is already exported from askcc/definitions.py:699.
Risks and Open Questions
model: inheritat the CLI/env layer —VALID_FRONTMATTER_MODELSincludes"inherit". Reusing the same choice set for the CLI flag and env var keeps the surface symmetric, butinheritis meaningful primarily inside subagent frontmatter (it tells claude to use the parent session's model). At the askcc-CLI level,inheritreaching the runner becomes--model inheritagainstclaude, which may or may not be accepted. Plan: pass through verbatim. If a downstream rejection emerges, address as a follow-up rather than special-casing askcc.- Unset-everywhere fallback — when no CLI / env / frontmatter value exists,
_resolve_modelreturnsNoneand_frontmatter_cli_flagsemits no--modelflag, lettingclaudepick its own default. Identical to today's behavior for templates withoutmodel:, so no behavior change for users who haven't customized.
Non-Goals
- Does not add
--max-turns/ASKCC_CLAUDE_MAX_TURNS(separate symmetry gap; can be a follow-up if needed) - Does not surface
permission_mode— intentionally omitted in ✨ Add subagent frontmatter to agent action definitions #84 because askcc runsclaude --dangerously-skip-permissionsand a permission prompt would deadlock an unattended run
References
- feat: strengthen agent action definitions using Claude Code subagent frontmatter conventions #80 — parent feature (frontmatter conventions)
- PR ✨ Add subagent frontmatter to agent action definitions #84 — initial implementation that merged the bulk of feat: strengthen agent action definitions using Claude Code subagent frontmatter conventions #80
askcc/cli.py:59-83— existing_resolve_effort/_resolve_max_thinking_tokenspattern to mirroraskcc/runners.py:49-60—_frontmatter_cli_flagsemission siteaskcc/definitions.py:699—VALID_FRONTMATTER_MODELSsource of truthtests/test_askcc.py:2598-2648—TestEffortPrecedencepattern to mirror forTestModelPrecedencetests/test_askcc.py:2651-2702—TestRunnerFrontmatterFlagsto extend
Activity
monkut commentedon May 16, 2026
Plan:
--modelCLI flag andASKCC_CLAUDE_MODELenv varIssue body rewritten to development-ready. Mirrors the existing
--effort/ASKCC_CLAUDE_EFFORT_LEVELprecedence chain — the CLI resolves precedence, the runner consumes the resolved value.Sections added / updated
## Implementation Plan— 7 steps withfile:linereferences (argparse flag,_resolve_modelhelper, runner wiring, README, tests).## Verification—uv run pytest/ruff check/pyrightplus manual smoke commands exercising CLI / env / invalid-env / invalid-CLI paths.## Dependencies— none (self-contained additive change).## Risks and Open Questions—model: inheritsemantics at the CLI layer; unset-everywhere fallback behavior.## Acceptance Criteria— original five items retained; extended with explicit README-table coverage and a green-check requirement for the lint/type/test trio.## Current State— added a line referencingVALID_FRONTMATTER_MODELSas the shared source of truth.Key decisions
runner.rungains amodel: str | Nonekeyword arg; precedence resolution lives inaskcc/cli.py(_resolve_model), matching howeffort_levelandmax_thinking_tokensflow today._resolve_modelreturnsstr | None, not a default. Unset everywhere → no--modelflag emitted;claudepicks its own default. Preserves today's behavior for templates withoutmodel:.choices=VALID_FRONTMATTER_MODELShandles invalid-CLI rejection for free; env-var validation uses warn-and-ignore mirroring_resolve_effort(askcc/cli.py:59-71).Open question for the author / reviewer
model: inheritbe filtered out at the askcc-CLI layer, or passed through toclaudeverbatim? Plan currently passes through verbatim —inheritis semantically a subagent-frontmatter concept and may not work as--model inheriton the parent claude invocation. Treating any rejection as a follow-up rather than a special case keeps this change tight. Happy to filter at askcc's layer if preferred.Ready for development.
:tokens-used: model: N/A, input: 50, output: 17685
monkut commentedon May 16, 2026
Implemented in PR #111.
Changes
askcc/cli.py— added--modelargparse flag (choices=VALID_FRONTMATTER_MODELS),_resolve_modelhelper mirroring_resolve_effort, and wired the resolved value into therunner.run(...)call alongsideeffort_level/max_thinking_tokens.askcc/runners.py—Runner.run(abstract) andClaudeRunner.runacceptmodel: str | None = None;_frontmatter_cli_flagstakes an optional override and emits--modelonly when truthy (preserves the "no flag when unset" behavior).README.md— generalized the "Override Precedence" section to covermodel; added rows for--model(global options) andASKCC_CLAUDE_MODEL(env vars).tests/test_askcc.py— addedTestModelPrecedence,TestModelCLIFlags,TestResolveModel, and extendedTestRunnerFrontmatterFlagswith override / fallback coverage.Precedence chain (mirrors
--effort)CLI flag (
--model) > env var (ASKCC_CLAUDE_MODEL) > template frontmatter > unset (no--modelflag emitted,claudepicks its own default).Verification
uv run pytest— 272 passeduv run ruff check— all checks passeduv run pyright— 0 errors, 0 warningsDesign decisions
_resolve_modelreturnsstr | None(no built-in default). Unset everywhere → no--modelflag emitted, matching today's behavior for templates withoutmodel:.choices=VALID_FRONTMATTER_MODELShandles invalid-CLI rejection; env var uses warn-and-ignore mirroring_resolve_effort.model: inheritpasses through verbatim — not filtered at the askcc-CLI layer as noted in the open question. If a downstream rejection emerges it can be addressed as a follow-up.:tokens-used: model: N/A, input: 73, output: 18472
Merge pull request #111 from monkut/feature/110-add-model-cli-flag