Skip to content

feat(cli): reject model overrides the agent's CLI won't accept - #695

Merged
bborn merged 2 commits into
mainfrom
task/2-taskyou-should-not-allow-setting-a-model
Aug 26, 2026
Merged

feat(cli): reject model overrides the agent's CLI won't accept#695
bborn merged 2 commits into
mainfrom
task/2-taskyou-should-not-allow-setting-a-model

Conversation

@bborn

@bborn bborn commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Problem

ty create --model <anything> was accepted. db.IsValidModel was literally return true, on the documented theory that the Claude CLI validates the model name itself.

It does — but inside tmux, after the task has launched, where nobody is looking. The task moves to running, claude exits on the bad flag, and the card sits there looking busy. The claude executor slug that an early default baked into every row (the one-time repair in sqlite.go) is exactly this failure.

What changed

Validation up front, in internal/db/models.go:

  • Aliasesopus, sonnet, haiku, fable, opusplan, with an optional [1m] variant suffix.
  • Full IDs — anything carrying the executor's vendor prefix and a sane shape, so claude-opus-5, claude-haiku-4-5-20251001 and grok-4-fast pass without ty tracking every release. A model that ships next month works with no code change.
  • Rejected — typos (opuss), other vendors (gpt-5 on claude, opus on grok), and the executor slug (claude). The error names the alternatives.
  • Executors with no --model flag — codex, gemini, pi, opencode, openclaw never pass the flag to their CLI, so an override there is dead config. They now say so instead of dropping it silently.
$ ty create "x" --model opuss
Error: unknown claude model "opuss" — known models: opus, sonnet, haiku, fable, opusplan (or any claude-* model ID)

$ ty create "x" -e codex --model gpt-5-codex
Error: the codex executor has no --model flag, so "gpt-5-codex" would be silently ignored (model overrides work with: claude, grok)

Every write path that takes a model now goes through one entry point, db.ValidateTaskModel:

Path Behaviour
ty create --model rejects with exit 1; completion offers the chosen executor's models
PATCH /api/tasks/{id} (GUI and any HTTP client) 400, nothing persisted
Workflow YAML (ty pipeline) rejected at parse time — step "Code": unknown claude model …
taskyou_create_task (MCP) nothing to do — it takes neither model nor executor

The API validates only when the request is actually setting a model, and against the executor as it stands after the update, so an edit that only touches the title is never blocked by a model stored before this check existed.

What stays unchecked, deliberately

  • Proxy-routed tasks. A CLAUDE_CONFIG_DIR override or ANTHROPIC_BASE_URL (the ollama shape, already exercised in pipeline_test.go with glm-5.2:cloud) means the model names are the proxy's, which ty cannot know. ValidateTaskModel resolves that hatch from the task's own config dir and env, its project's config dir, and an ambient ANTHROPIC_BASE_URL — a workflow step's config_dir/env does the same at parse time.
  • The TUI form and CreateTask. The check lives at the write boundaries, not in CreateTask, so the form (whose picker only offers valid values) and any proxy-routed caller are unaffected. IsValidModel survives as the looser executor-less form the form's remembered per-project default uses — it now rejects the claude slug instead of waving it through.

Files

  • internal/db/models.go (new) — constants, per-executor registry, ValidateModel, ValidateTaskModel, ModelBackendIsCustom; the model block moves here out of tasks.go.
  • cmd/task/main.go — validate in ty create, executor-aware shell completion.
  • internal/web/handlers.go — validate in handleUpdateTask.
  • internal/pipeline/definition_file.go — validate each step's model at parse time.
  • Tests: internal/db/models_test.go (new), internal/web/model_validation_test.go (new), internal/pipeline/custom_test.go, cmd/task/cli_test.go.

Verification

go vet ./... clean; the full go test ./... passes package-by-package (the suite OOMs when every package builds at once on this box — unrelated to this change; -p 1 works). Smoke-tested against a real binary: every rejection above exits 1, and opus / claude-opus-5 / claude-opus-9 / grok-4 / no-override / ANTHROPIC_BASE_URL=… --model glm-5.2:cloud all create.

Maintenance note

New Claude Code aliases still need a line in executorModelAliases — that's how fable arrived alongside opus/sonnet/haiku. New full model IDs need nothing.

🤖 Generated with Claude Code

bborn and others added 2 commits August 26, 2026 12:12
`ty create --model` accepted any string: IsValidModel returned true
unconditionally, on the theory that the Claude CLI validates the name
itself. It does — but inside tmux, after the task has launched, where
nobody sees it. The task just sits there looking busy. The "claude"
executor slug an early default baked into every row (see the
clear-model repair in sqlite.go) is that failure mode.

Validate the override up front instead. A model passes when it is one
of the executor's aliases (opus/sonnet/haiku/fable/opusplan, with an
optional [1m] variant) or a well-formed ID carrying the executor's
vendor prefix, so claude-opus-5 and grok-4-fast work without ty having
to track every release. Typos, other vendors' models, and the executor
slug are rejected with a message naming the alternatives. Executors
with no --model flag at all (codex, gemini, pi, opencode, openclaw)
now say so rather than silently dropping the override.

Two things stay unchecked on purpose: a task routed at a proxy — a
CLAUDE_CONFIG_DIR override or ANTHROPIC_BASE_URL, the ollama shape —
names the proxy's models (glm-5.2:cloud), which ty cannot know; and
the TUI form, whose picker only offers valid values anyway.

Workflow YAML gets the same check at parse time, with the same proxy
escape hatch, since a bad model there stalls a step just as quietly.
Shell completion now offers the chosen executor's models.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI check left the HTTP path open: PATCH /api/tasks/{id} wrote
whatever `model` it was handed, so the GUI (and any client) could
still park a task on a model the agent's CLI rejects.

Rather than repeat the CLI's escape-hatch logic, give both paths one
entry point: db.ValidateTaskModel resolves the proxy hatch from the
task's own config dir and env, its project's config dir, and an
ambient ANTHROPIC_BASE_URL, then defers to ValidateModel. `ty create`
now builds a probe task and calls it, dropping its local helper.

The API validates only when the request is actually setting a model,
and against the executor as it stands after the update — an edit that
touches the title must not be blocked by a model stored before this
check existed.

MCP needs nothing: taskyou_create_task takes neither model nor
executor, so there is no unvalidated write there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bborn
bborn merged commit ea94c56 into main Aug 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant