Crush config manage models
You might have skills, but I got quirks!
npx -y skills add detro/quirks --skill crush-config-manage-modelsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when the user wants to update, refresh, or sync the model definitions in their Crush config (crush.json) for one or more providers from the upstream charmbracelet/catwalk definitions — including composite/reseller providers like Vertex AI or Bedrock that combine models from other providers (e.g. "update the models used by anthropic", "add to my vertexai all the gemini and anthropic models", or "update all the models I already use with the latest version").
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
20.8 KB, as published. Nobody here has run it
Refresh Crush Models
Authored and maintained by Ivan De Marino.
Scope: This skill is specific to the Crush coding agent (
charmbracelet/crush) and itscrush.jsonconfiguration file. It is not applicable to other agents or config formats. Crucially, it is limited to providers that have an authoritative configuration defined in the upstreamcharmbracelet/catwalkrepository; arbitrary or custom providers not present in catwalk cannot be refreshed or added.
This skill updates the per-provider models arrays inside the user's Crush
configuration (crush.json) using the authoritative model definitions published
on the main branch of
charmbracelet/catwalk
(the model catalog used by charmbracelet/crush).
Each catwalk provider is a JSON file at
internal/providers/configs/<provider>.json whose models array uses the same
object schema Crush expects (id, name, cost_per_1m_in,
cost_per_1m_out, cost_per_1m_in_cached, cost_per_1m_out_cached,
context_window, default_max_tokens, can_reason, reasoning_levels,
default_reasoning_effort, supports_attachments, ...). No field translation is
needed — catwalk model objects drop straight into Crush.
How the work is split
- Fetching is done by YOU (the agent) with Crush's built-in
fetch/downloadtool — not by the script and not withgh/curl. Each provider file is fetched from its raw URL:
Save each fetched file locally (e.g.https://raw.githubusercontent.com/charmbracelet/catwalk/main/internal/providers/configs/<provider>.jsondownloadit to/tmp/catwalk/<provider>.json, orfetchit and write the body to a file). - All JSON math (compose / dedup / restrict / merge / apply) is done by the
helper script, which reads those local files and never touches the network:
scripts/catwalk.sh {models|meta|compose|restrict|merge|apply} <file> ... - Editing
crush.jsonis done by YOU with Crush'sview/edit/writetools, after showing the diff.
The only external dependency is jq (used by the script; the builtin jq
skill documents it). No gh, no curl, no network access in the script.
jq portability — never use
--slurpfile. Somejqbuilds (notably certain Homebrew 1.8.x binaries) reject--slurpfile(and even--version) withjq: function not defined: slurpfile/0. The helper script already avoids it, and any ad-hoc jq YOU run must avoid it too. To pull a JSON file into a jq variable, read it with--argjsonand a command substitution instead:# WRONG (may fail on some jq builds): jq --slurpfile cur cur.json 'MAP USING $cur[0]' new.json # RIGHT (portable everywhere): jq --argjson cur "$(cat cur.json)" 'MAP USING $cur' new.jsonBetter still, prefer the
restrictsubcommand (below) so you don't hand-roll id-intersection jq at all.
Listing available providers
There is no list subcommand (it would need network). If you need to discover
the full provider set, fetch the directory listing page
https://github.com/charmbracelet/catwalk/tree/main/internal/providers/configs
(or its API form) and read the file names. For a named provider you already
know, just fetch its raw <provider>.json directly — no listing needed.
Core rules (do not deviate)
- Source of truth is catwalk
main. Always fetch fresh; never rely on memorized model lists. - Upstream wins. When refreshing, the catwalk definition fully replaces the
user's existing entry for a given model id. Local customizations (custom
cost, renamed
name, tweakedcontext_window, etc.) are discarded — they are assumed to be staler than catwalk. - Retired models are removed. If a model id the user currently has is no longer present in the (composed) upstream set, drop it.
- Composite / reseller providers compose from source files. Providers like
vertexai(Google Vertex AI),bedrock-*(AWS Bedrock),azure, etc. resell models that originate from other providers (Gemini, Anthropic, OpenAI, ...). For these, build the model list by composing:- start from the reseller's own catwalk file (e.g.
vertexai.json), then - add the requested source providers' models (e.g.
gemini+anthropic), - deduplicate by
id, and the SOURCE providers WIN over the reseller's base entry on conflict. Rationale: when the user asks to pull from gemini/anthropic directly, they know those files are more up to date than the reseller's curated copy. The helpercompose <base.json> <src.json>...implements exactly this (base first, sources override, first-seen order preserved).
- start from the reseller's own catwalk file (e.g.
- Show the diff, then edit. Present the added / removed / updated model ids
(the
mergesubcommand prints this summary), then editcrush.jsonwith Crush's own edit tools. No backup file. This is ordinary file editing. - Only touch the targeted provider(s). Never reorder, reformat, or modify
other parts of
crush.json(lsp, permissions, other providers, etc.). Preserve the file's existing indentation/style.
Locating crush.json
Resolve the active Crush config path the same way Crush documents it (https://github.com/charmbracelet/crush#configuration). Check, in order, and use the first that exists:
$CRUSH_CONFIG/$CRUSH_GLOBAL_CONFIG(or an explicit path the user gives).- Project-local:
./crush.jsonor./.crush.jsonin the working dir. - Global, by OS:
- macOS / Linux:
$XDG_CONFIG_HOME/crush/crush.json, else~/.config/crush/crush.json. - Windows:
%USERPROFILE%\AppData\Local\crush\crush.json(i.e.$LOCALAPPDATA/crush/crush.json).
- macOS / Linux:
If both a project-local and a global config exist, ask which to update unless the user already made it clear. Read the file before editing it.
Determining the request mode
Mode 1 — Explicit single provider
"update the models used by anthropic" / "refresh gemini models"
The provider maps directly to one catwalk file.
- Fetch
…/configs/<provider>.json(Crushfetch/download) to a local file. scripts/catwalk.sh models <provider>.json→ the desired model array.- Continue at Apply the update.
Mode 2 — Explicit composition (reseller + named sources)
"add to my vertexai all the gemini and anthropic models"
- Fetch the reseller and each source file locally (e.g.
vertexai.json,gemini.json,anthropic.json). scripts/catwalk.sh compose <reseller>.json <src1>.json <src2>.json ...→ desired model array.- Base = the reseller; sources = the named providers; sources win on dedup.
- Continue at Apply the update.
Mode 3 — Auto / minimal input
"update all the models I already use, with the latest version"
Figure out, per provider already present in crush.json, what to fetch:
- Read the user's
crush.jsonand list every provider underprovidersthat has amodelsarray (or that you can refresh). - For each such provider, decide its catwalk sources, then fetch the needed
<provider>.jsonfile(s) locally:- If the provider name matches a catwalk file directly (e.g.
anthropic,gemini,openai) and is not a known reseller → Mode-1 style: fetch that provider's file and take its models. - If it's a reseller (
vertexai,bedrock-*,azure, ...) → infer the source providers from the ids already configured:- ids starting with
gemini⇒ sourcegemini - ids starting with
claude⇒ sourceanthropic - ids starting with
gpt/o1/o3/o4⇒ sourceopenai - (extend by inspecting catwalk: a model id present in provider X's
modelsidentifies X as a source.) Fetch the reseller file plus each inferred source file, thencompose <reseller>.json <inferred-sources>.json ...(superset, sources win) — do not silently fall back to only the reseller's curated file; the user-confirmed behavior is the composed superset.
- ids starting with
- Use
scripts/catwalk.sh meta <provider>.jsonandmodels <provider>.jsonto confirm a provider'stypeand ids when inferring sources.
- If the provider name matches a catwalk file directly (e.g.
- Apply the update for each provider (below). Summarize all providers touched.
If inference is genuinely ambiguous for a reseller (e.g. ids you can't map to a known source), state what you found and ask which source providers to use rather than guessing.
Filtering / narrowing the model set (optional)
The user may want only a SUBSET of the composed/desired models, e.g.:
"only include opus and sonnet versions 4.6 and higher" "only include the latest version of each model variant" "drop the preview models" / "gemini flash only"
Apply the filter with jq on the composed/desired array (the /tmp/new.json
produced by models/compose) before running merge. The script does no
filtering; you express the user's intent as a jq pipeline. Keep the result a
plain models array so merge/apply keep working unchanged.
The catwalk model objects carry useful fields for filtering: id, name,
context_window, can_reason, supports_attachments, cost fields, etc. Model
ids encode the variant and version (e.g. claude-opus-4-8,
claude-sonnet-4-5-20250929, gemini-3.5-flash, gemini-3-flash-preview).
A. Explicit variant + version-floor filters
"only opus and sonnet, 4.6 and higher"
Parse a comparable numeric version out of each id, then keep matching variants
at or above the floor. Example (Anthropic-style claude-<variant>-<maj>-<min>…):
jq '
[ .[]
| . as $m
| ($m.id | capture("claude-(?<variant>opus|sonnet|haiku)-(?<maj>[0-9]+)-(?<min>[0-9]+)") ) as $v
| select($v != null)
| select($v.variant | IN("opus","sonnet"))
| select((($v.maj|tonumber) > 4) or
(($v.maj|tonumber) == 4 and ($v.min|tonumber) >= 6))
| $m
]
' /tmp/new.json > /tmp/filtered.json
Adjust the regex/floor to the provider's id scheme and the user's request (e.g.
gemini-(?<maj>[0-9]+)\\.?(?<min>[0-9]*)). State the floor and variants you
applied so the user can confirm.
Pitfall: ids that append a release date (e.g.
claude-opus-4-20250514) put the date where a minor version is expected, so a naivemin >= 6test will wrongly admit…-4-20250514(min "parsed" as a huge date number). Inspect ids first (jq -r '.[].id' /tmp/new.json) and make the regex distinguish a real minor (-4-6) from a date (-4-YYYYMMDD) — e.g. require the version segment to be 1–2 digits, and treat dated ids separately.
B. "Latest version of each variant only"
"only the latest opus, latest sonnet, latest gemini-flash, etc."
Group ids by their variant key (the id with the version/date segments
stripped), then keep the single highest-versioned id per group. Derive a
sortable version key from the id (numeric version tuples and any trailing date
YYYYMMDD both compare correctly as zero-padded/lexible tuples). Example:
jq '
def variant_key: (sub("-[0-9].*$";"")); # e.g. claude-opus, gemini-flash
def version_key:
[ (scan("[0-9]+") | tonumber) ]; # numeric segments as a sortable list
[ .[] ]
| group_by(.id | variant_key)
| map( max_by(.id | version_key) )
' /tmp/new.json > /tmp/filtered.json
Tune variant_key so it separates the variants the user means (for Gemini you
may want pro vs flash to stay distinct, and preview vs GA — inspect the ids
first with jq -r ".[].id" /tmp/new.json). If "latest" is ambiguous between a
dated GA release and a -preview, prefer GA unless the user says otherwise, and
say which you picked.
Pitfall: the simple
variant_key/version_keyabove are only a starting point.sub("-[0-9].*$";"")groupsgemini-3-pro-previewandgemini-3-flash-previewseparately (good) but won't splitprofromflashif the family number comes last, and aversion_keybuilt from all numeric runs ranks a trailing date (…-20250514) above a plain-4-8. Verify the chosen winner per group against the printed id list, and refine the keys (e.g. include the variant word, or weight version vs date) until each group's "latest" is the one the user expects.
C. Refresh ONLY the ids already configured (restrict)
"make sure the definition of each model I have is latest" / "just update what I already use, don't add new models"
This is a very common intent (and the natural reading of Mode 3): keep the user's existing set of model ids, but replace each definition with the upstream one and drop any id that has been retired upstream — without adding every other model the upstream provider now ships.
Don't hand-roll this with --slurpfile. Use the restrict subcommand: it
filters a desired/composed array down to only the ids present in a "keep" file
(a models array, a full provider object, or a bare array of id strings), using
portable jq only.
S=~/.agents/skills/crush-config-manage-models/scripts/catwalk.sh
# desired upstream set (one provider, or a composed reseller superset):
"$S" compose /tmp/catwalk/anthropic.json /tmp/catwalk/gemini.json > /tmp/new.json
# the user's current array for this provider:
jq '.providers.<provider>.models // []' crush.json > /tmp/cur.json
# keep only the ids the user already has (retired ids drop, nothing new added):
"$S" restrict /tmp/new.json /tmp/cur.json > /tmp/filtered.json
Then continue at After filtering using /tmp/filtered.json.
After filtering
Use /tmp/filtered.json in place of /tmp/new.json from step 2 of Apply the
update onward (merge /tmp/cur.json /tmp/filtered.json …). The merge summary
and final recap then reflect exactly the narrowed set, and any of the user's
existing ids that fall outside the filter are correctly reported as removed.
If a filter would yield an empty set, or you can't confidently parse the ids for the requested rule, show the candidate ids and ask rather than guessing.
Apply the update
For each provider being updated:
- Write the provider's current models array to a temp file (use Crush's
tools, or jq):
jq '.providers.<provider>.models // []' <crush.json> > /tmp/cur.json - Write the desired array (from the
modelsorcomposesubcommand, run on the locally fetched catwalk files) to a temp file:... > /tmp/new.json scripts/catwalk.sh merge /tmp/cur.json /tmp/new.json > /tmp/merged.json- The merge summary (added
+/ removed-/ kept-updated~) is printed to stderr — relay it to the user as the diff. /tmp/merged.jsonis the authoritative merged array (upstream wins, retired removed).
- The merge summary (added
- Present the diff to the user (the summary). Then set
providers.<provider>.modelsto the merged array. Use theapplysubcommand, which preserves the rest of the document and works on any jq version:
Verify the change is scoped to that provider's models before saving:scripts/catwalk.sh apply <crush.json> <provider> /tmp/merged.json > /tmp/out.json
then writediff <(jq 'del(.providers.<provider>.models)' <crush.json) \ <(jq 'del(.providers.<provider>.models)' /tmp/out.json) # must be empty/tmp/out.jsonback overcrush.jsonusing Crush'swrite/edittool. If you instead hand-editcrush.jsondirectly, match the file's existing indentation exactly and change only that provider'smodelsblock. - If the provider didn't previously exist in
providers, create the minimal block ("<provider>": { "models": [...] }) — but only if the user asked to add it; otherwise stick to providers already configured.
Final recap (always)
After all targeted providers have been written back, you MUST always end with a recap of the models now activated for each provider so the user can see the end state at a glance. This recap is mandatory on every run — even when only one provider changed, or when nothing changed.
For each provider you touched, read the saved crush.json and enumerate the
model ids now present:
jq -r '.providers.<provider>.models[].id' crush.json
Format the recap as a list: one section (or sub-list) per provider, and under each provider a bulleted list of the model ids now activated. For example:
Activated models:
- litellm (7):
- claude-opus-4-8
- claude-sonnet-4-6
- …
- vertexai (9):
- gemini-3.1-pro-preview
- …
Alongside each provider's list, include the counts / change summary (added, removed, kept-updated) for context. This recap is mandatory and comes in addition to the pre-write diff from step 4.
Notes & gotchas
- API keys / endpoints / defaults are not touched. This skill only manages
models. Leaveapi_key,api_endpoint,default_large_model_id, etc. as the user has them. (catwalk'smetacarries upstream defaults if the user asks to also refresh those, but do it only on explicit request.) - Reseller list isn't hard-coded in catwalk by name — identify resellers by
their
type(google-vertex,bedrock,azure,openrouter,vercel,openai-compat) and by the fact that their models originate elsewhere. Usescripts/catwalk.sh meta <provider>.jsonto readtype. - Duplicate ids in the user's existing config are harmless to the merge (it dedups), and collapsing them to a single canonical entry is desirable.
- Validate JSON after editing (
jq . crush.json), and if the repo/dir is a git checkout, showgit diffofcrush.jsonso the change is reviewable. - Only
jqis required. Fetching uses Crush'sfetch/downloadtool; editing uses Crush'sedit/writetools. Noghorcurlis needed.
Automating & Protecting with Crush Hooks
By leveraging Crush's native hook system (as described in the crush-hooks skill), you can streamline model refreshing and prevent config corruption.
1. Auto-approving catwalk.sh operations
To allow this skill to run catwalk.sh without interactive prompts, add a PreToolUse hook matching ^bash$:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^bash$",
"command": "if echo \"$CRUSH_TOOL_INPUT_COMMAND\" | grep -q 'catwalk.sh'; then echo '{\"decision\":\"allow\"}'; fi"
}
]
}
}
2. Safeguarding crush.json from invalid JSON writes
To prevent accidental corruption of crush.json during the update process, add a PreToolUse hook that validates that any proposed write or edit keeps crush.json as valid JSON:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(write|edit|multiedit)$",
"command": "if [[ \"$CRUSH_TOOL_INPUT_FILE_PATH\" == *crush.json ]]; then if [[ \"$CRUSH_TOOL_NAME\" == \"write\" ]]; then read -r input; content=$(echo \"$input\" | jq -r '.tool_input.content'); if ! echo \"$content\" | jq . >/dev/null 2>&1; then echo \"Aborting: invalid JSON for crush.json\" >&2; exit 2; fi; fi; fi"
}
]
}
}
3. Complementing "disable_provider_auto_update"
Under options in crush.json, the disable_provider_auto_update boolean can be set to true to stop Crush from auto-refreshing its built-in model definitions on its own. When auto-updates are disabled, utilizing this manual skill is the primary, reliable way to keep models synchronized with upstream catwalk definitions.
Quick reference
Fetch first (Crush fetch/download), saving to local files, then:
S=~/.agents/skills/crush-config-manage-models/scripts/catwalk.sh
# (agent fetched these to /tmp/catwalk/<provider>.json beforehand)
"$S" meta /tmp/catwalk/vertexai.json # provider metadata incl. type
"$S" models /tmp/catwalk/anthropic.json # one provider's models array
"$S" compose /tmp/catwalk/vertexai.json \
/tmp/catwalk/gemini.json \
/tmp/catwalk/anthropic.json # reseller + sources, sources win
"$S" restrict /tmp/new.json /tmp/cur.json # keep only ids already configured
"$S" merge /tmp/cur.json /tmp/new.json # merged array + diff summary(stderr)
"$S" apply crush.json vertexai /tmp/merged.json # full doc w/ models replaced
Pass JSON into ad-hoc jq with
--argjson NAME "$(cat file.json)", never with--slurpfile(see the jq-portability note near the top).