Skip to content

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

Closed

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 flag — askcc/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 signature — askcc/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_flags — askcc/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 README — README.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. Tests — tests/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 layer — VALID_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

Activity

  1. self-assigned this
    on May 16, 2026
  2. monkut commented on May 16, 2026

    @monkut
    OwnerAuthor

    Plan: --model CLI flag and ASKCC_CLAUDE_MODEL env var

    Issue body rewritten to development-ready. Mirrors the existing --effort / ASKCC_CLAUDE_EFFORT_LEVEL precedence chain — the CLI resolves precedence, the runner consumes the resolved value.

    Sections added / updated

    • ## Implementation Plan — 7 steps with file:line references (argparse flag, _resolve_model helper, runner wiring, README, tests).
    • ## Verification — uv run pytest / ruff check / pyright plus manual smoke commands exercising CLI / env / invalid-env / invalid-CLI paths.
    • ## Dependencies — none (self-contained additive change).
    • ## Risks and Open Questions — model: inherit semantics 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 referencing VALID_FRONTMATTER_MODELS as the shared source of truth.

    Key decisions

    • Runner contract stays symmetric. runner.run gains a model: str | None keyword arg; precedence resolution lives in askcc/cli.py (_resolve_model), matching how effort_level and max_thinking_tokens flow today.
    • _resolve_model returns str | None, not a default. Unset everywhere → no --model flag emitted; claude picks its own default. Preserves today's behavior for templates without model:.
    • Argparse choices=VALID_FRONTMATTER_MODELS handles 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

    • Should model: inherit be filtered out at the askcc-CLI layer, or passed through to claude verbatim? Plan currently passes through verbatim — inherit is semantically a subagent-frontmatter concept and may not work as --model inherit on 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

  3. added
    action:developIssue is ready for development
    and removed
    action:planIssue is ready for planning
    on May 16, 2026
  4. monkut commented on May 16, 2026

    @monkut
    OwnerAuthor

    Implemented in PR #111.

    Changes

    • askcc/cli.py — added --model argparse flag (choices=VALID_FRONTMATTER_MODELS), _resolve_model helper mirroring _resolve_effort, and wired the resolved value into the runner.run(...) call alongside effort_level / max_thinking_tokens.
    • askcc/runners.py — Runner.run (abstract) and ClaudeRunner.run accept model: str | None = None; _frontmatter_cli_flags takes an optional override and emits --model only when truthy (preserves the "no flag when unset" behavior).
    • README.md — generalized the "Override Precedence" section to cover model; added rows for --model (global options) and ASKCC_CLAUDE_MODEL (env vars).
    • tests/test_askcc.py — added TestModelPrecedence, TestModelCLIFlags, TestResolveModel, and extended TestRunnerFrontmatterFlags with override / fallback coverage.

    Precedence chain (mirrors --effort)

    CLI flag (--model) > env var (ASKCC_CLAUDE_MODEL) > template frontmatter > unset (no --model flag emitted, claude picks its own default).

    Verification

    • uv run pytest — 272 passed
    • uv run ruff check — all checks passed
    • uv run pyright — 0 errors, 0 warnings

    Design decisions

    • _resolve_model returns str | None (no built-in default). Unset everywhere → no --model flag emitted, matching today's behavior for templates without model:.
    • Argparse choices=VALID_FRONTMATTER_MODELS handles invalid-CLI rejection; env var uses warn-and-ignore mirroring _resolve_effort.
    • model: inherit passes 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

  5. added and removed
    action:developIssue is ready for development
    on May 16, 2026
  6. added a commit that references this issue on May 18, 2026
    8995d80
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions