Skip to content

feat: add --model CLI flag and ASKCC_CLAUDE_MODEL env var to override per-action frontmatter #110

@monkut

Description

@monkut

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 --model from config.model only — no CLI/env consultation.
  • askcc/cli.py exposes --effort and --max-thinking-tokens argparse flags but no --model.
  • askcc/settings.py reads ASKCC_CLAUDE_EFFORT_LEVEL and ASKCC_CLAUDE_MAX_THINKING_TOKENS but no ASKCC_CLAUDE_MODEL.
  • README.md "Override Precedence" section documents the chain for effort and max_thinking_tokens; model is 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.

  1. Add --model argparse flagaskcc/cli.py near the existing --effort argument (askcc/cli.py:139-146). Use choices=VALID_FRONTMATTER_MODELS (imported from askcc/definitions.py:699) so argparse rejects invalid values pre-run. Default None. Help text documents the precedence: CLI > env (ASKCC_CLAUDE_MODEL) > frontmatter > claude's built-in default.

  2. Add _resolve_model helper — new function in askcc/cli.py immediately after _resolve_max_thinking_tokens (around askcc/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_MODEL if non-empty and in VALID_FRONTMATTER_MODELS (warn-and-ignore on invalid, matching _resolve_effort's warning shape)
    • else frontmatter_value (may itself be None)
    • Return None when no source is set — caller suppresses the --model flag.
  3. Wire resolved value into runner call — in askcc/cli.py main() near askcc/cli.py:286-287 where effort_level / max_thinking_tokens are resolved, add model = _resolve_model(args.model, config.model) and pass model=model into the runner.run(...) call at askcc/cli.py:290-299.

  4. Extend Runner.run signatureaskcc/runners.py:33-46: add model: str | None = None keyword-only parameter to the abstract method and to ClaudeRunner.run (askcc/runners.py:159-170). Inside ClaudeRunner.run, forward it to _frontmatter_cli_flags so the resolved value (not config.model) drives the emitted flag.

  5. Update _frontmatter_cli_flagsaskcc/runners.py:49-60: accept an optional model: str | None = None parameter. Replace the config.model lookup with effective_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 without model:.

  6. Document in READMEREADME.md:

    • "Override Precedence" section (lines 236-244): generalize so it covers effort, max_thinking_tokens, and model.
    • Global options table (lines 110-122): add a --model row.
    • Environment Variables table (lines 134-148): add an ASKCC_CLAUDE_MODEL row.
  7. Teststests/test_askcc.py:

    • TestModelPrecedence (new class) mirroring TestEffortPrecedence (tests/test_askcc.py:2598-2648): four tests covering CLI > env > frontmatter > unset (no default; None propagates).
    • TestThinkingSettings extensions (tests/test_askcc.py:1827): test_invalid_askcc_claude_model_logged_and_ignored mirroring test_invalid_effort_level_logged_and_ignored (tests/test_askcc.py:1846-1853), but exercising _resolve_model (env lookup lives in cli.py, not settings.py — adjust import and caplog logger accordingly).
    • TestRunnerFrontmatterFlags extensions (tests/test_askcc.py:2651-2702): test_model_override_emitted (runner.run with model="sonnet" overrides config.model="opus") and test_model_falls_back_to_config_when_unset (no model kwarg → config.model still drives the flag).
    • CLI-level forwarding test following the pattern at tests/test_askcc.py:2030-2044: assert --model sonnet and ASKCC_CLAUDE_MODEL=sonnet both reach runner.run(model="sonnet").
    • Argparse rejection test: invoking main() with --model opuz raises SystemExit (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 regressions

Manual 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> overrides model: opus in DEVELOP_SYSTEM_PROMPT.md frontmatter
  • ASKCC_CLAUDE_MODEL=sonnet askcc develop <url> does the same when no --model flag is set
  • Invalid values: --model opuz rejected by argparse; ASKCC_CLAUDE_MODEL=opuz logs a warning and falls through to frontmatter/default
  • README.md "Override Precedence" section extended to cover model alongside effort and max_thinking_tokens
  • README.md Environment Variables table lists ASKCC_CLAUDE_MODEL; global options table lists --model
  • Tests cover: CLI > env > frontmatter > unset precedence; invalid env warn-and-ignore; runner emits/omits --model correctly; argparse rejects invalid CLI choice
  • uv run pytest, uv run ruff check, uv run pyright all 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: inherit at the CLI/env layerVALID_FRONTMATTER_MODELS includes "inherit". Reusing the same choice set for the CLI flag and env var keeps the surface symmetric, but inherit is meaningful primarily inside subagent frontmatter (it tells claude to use the parent session's model). At the askcc-CLI level, inherit reaching the runner becomes --model inherit against claude, 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_model returns None and _frontmatter_cli_flags emits no --model flag, letting claude pick its own default. Identical to today's behavior for templates without model:, 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 runs claude --dangerously-skip-permissions and a permission prompt would deadlock an unattended run

References

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions