internal/skills/builtin/crush-config/SKILL.md
Crush supports two config formats:
crushrc — a Bash script that builds config by calling Crush builtins.
Preferred. Because it is real Bash you get includes, secrets,
conditionals, and variables for free.crush.json — static JSON. Fully supported; see
Legacy JSON format.Both are discovered together and deep-merged. Priority (highest to lowest):
.crushrc / crushrc / .crush.json / crush.json (project-local,
closer-to-cwd wins; Windows uses .\.crushrc / .\crushrc)$XDG_CONFIG_HOME/crush/crushrc or ~/.config/crush/crushrc
(%XDG_CONFIG_HOME%\crush\crushrc or
%USERPROFILE%\.config\crush\crushrc on Windows)Data directories (~/.local/share/crush and %LOCALAPPDATA%\crush) contain
machine-owned JSON state only; Crush does not discover or execute a crushrc
from those locations.
If a directory has both crushrc and crush.json, they merge (crushrc wins
on conflicts) and Crush logs a warning.
A crushrc is a plain Bash script executed at load time with the same embedded
shell the bash tool uses. It builds config by calling builtins (provider,
model, mcp, lsp, hook, permissions, option). Statements run top to
bottom; later statements win, and remove/reset operate on anything defined
earlier or pulled in via source.
#!/usr/bin/env bash
# Includes and secrets are just Bash.
source ~/.config/crush/shared.sh
provider add anthropic --api-key "$ANTHROPIC_API_KEY"
model large anthropic/claude-sonnet-4-20250514 --max-tokens 16384
model small anthropic/claude-haiku-4-20250514
option skill-path ./skills
permissions allow view ls grep edit
Values are ordinary Bash — quote and expand normally ("$VAR", $(cmd),
${VAR:?required}). A failing $(command) aborts the load.
CRUSH_VERSION is exported into the script so you can feature-detect the
running Crush (it is the literal devel for local builds):
[[ "$CRUSH_VERSION" != devel ]] && lsp add gopls --command gopls
All entity commands are verb-first. remove accepts rm as an alias. Booleans
accept true/false/1/0/yes/no, case-insensitive.
provider add <id> [flags] # define/update; repeated calls merge
provider remove <id> # alias: rm — removes the provider and its models
Flags: --name, --type (openai, openai-compat, anthropic, or a local
type like ollama, lmstudio, llamacpp), --api-key, --base-url,
--disable BOOL, --flat-rate BOOL, --discover-models BOOL,
--system-prompt-prefix TEXT, --extra-header KEY VALUE (repeatable),
--extra-body JSON, --provider-options JSON.
provider add deepseek \
--type openai-compat \
--base-url "https://api.deepseek.com/v1" \
--api-key "${DEEPSEEK_API_KEY:?set DEEPSEEK_API_KEY}"
model add <provider>/<id> [flags] # register a custom model (provider must exist)
model remove <provider>/<id> # alias: rm
model large [<provider>/<id>] [flags] # set the large slot; no arg prints it
model small [<provider>/<id>] [flags] # set the small slot; no arg prints it
<provider>/<id> is the same form crush models prints. A missing slash is
an error. model add requires the provider to already exist.model add flags: --name, --context-window N, --default-max-tokens N,
--can-reason BOOL, --supports-images BOOL, --price-input F,
--price-output F, --price-cache-create F, --price-cache-hit F,
--reasoning-effort low|medium|high.model large/model small flags: --think, --reasoning-effort,
--max-tokens N, --temperature F, --top-p F, --top-k N,
--frequency-penalty F, --presence-penalty F, --provider-options JSON.model large with no argument prints the current selection as provider/id,
usable in $(model large).large is the primary coding model; small is used for summarization.
mcp add <name> --type stdio|sse|http [flags] # default type is stdio
mcp remove <name> # alias: rm
Flags: --command CMD, --args ARG (repeatable), --env KEY VALUE
(repeatable), --url URL, --header KEY VALUE (repeatable), --timeout N,
--disabled BOOL, --disabled-tools TOOL (repeatable), --enabled-tools TOOL
(repeatable).
mcp add github --type http \
--url "https://api.githubcopilot.com/mcp/" \
--header Authorization "Bearer $GH_PAT"
mcp add filesystem --command node --args /path/to/mcp-server.js
lsp add <name> --command CMD [flags]
lsp remove <name> # alias: rm
Flags: --args ARG (repeatable), --env KEY VALUE (repeatable),
--filetypes TYPE (repeatable), --root-markers MARKER (repeatable),
--timeout N, --disabled BOOL, --init-options JSON, --options JSON.
lsp add go --command gopls --env GOPATH "$HOME/go"
lsp add typescript --command typescript-language-server --args --stdio
hook add <event> --command CMD [--name NAME] [--matcher REGEX] [--timeout N]
hook remove <event> [--name NAME] # alias: rm; without --name clears the event
Only named hooks can be removed individually — give a hook --name if you
intend to remove it later. See Hooks runtime for how hooks
execute (stdin payload, env vars, decisions).
hook add PreToolUse --matcher "^bash$" --command ".crush/hooks/no-haskell.sh" --name no-haskell
permissions allow <tool> [<tool> ...] # tools that skip permission prompts
permissions deny <tool> [<tool> ...] # hide tools from the agent entirely
deny is the inverse of allow: it writes options.disabled_tools. A denied
tool is hidden from the agent, not merely prompted for.
option <key> [value]
option reset <list-key> # clear a list option back to empty
true): debug, debug-lsp,
auto-lsp, progress.metrics,
auto-summarize, provider-auto-update,
default-providers. Example: option metrics false disables metrics.data-directory, initialize-as, notifications.attribution-trailer-style (none, co-authored-by,
assisted-by) and attribution-generated-with (boolean).option ui compact BOOL, option ui diff unified|split,
option ui transparent BOOL, option ui scrollbar default|always|never,
option ui completions-max-depth N, option ui completions-max-items N.context-path,
global-context-path, skill-path, disable-skill. Use option reset <key>
to wipe inherited values (e.g. after source).option progress false
option skill-path ./skills
option disable-skill crush-config
option attribution-trailer-style assisted-by
option attribution-generated-with true
option ui compact true
option ui diff unified
[!IMPORTANT] These skill paths are loaded by default and do NOT need
skill-path:.agents/skills,.crush/skills,.claude/skills,.cursor/skills.
Hooks are user-defined shell commands that fire on agent events. Currently only
PreToolUse is supported, which runs before a tool executes. This behavior is
the same however the hook is defined (hook add or JSON).
PreToolUse hooks with a matching
matcher (or no matcher) run in parallel.Event names are case-insensitive and accept snake_case: PreToolUse,
pretooluse, pre_tool_use, PRE_TOOL_USE all work.
{
"event": "PreToolUse",
"session_id": "abc-123",
"cwd": "/path/to/project",
"tool_name": "bash",
"tool_input": { "command": "ls -la" }
}
| Variable | Description |
|---|---|
CRUSH_EVENT | Event name (e.g. PreToolUse) |
CRUSH_TOOL_NAME | Name of the tool being called |
CRUSH_SESSION_ID | Current session ID |
CRUSH_CWD | Current working directory |
CRUSH_PROJECT_DIR | Project root directory |
CRUSH_TOOL_INPUT_COMMAND | Value of command from tool input (if present) |
CRUSH_TOOL_INPUT_FILE_PATH | Value of file_path from tool input (if present) |
Exit code 0 — hook succeeded. Stdout is parsed as JSON:
{ "decision": "allow", "context": "optional context appended to tool result" }
decision: allow to explicitly allow, deny to block, none (or omit).reason: explanation (used when denying).context: extra context appended to the tool result.updated_input: replacement JSON for the tool input; last non-empty wins.Exit code 2 — the tool call is blocked; stderr is the deny reason.
Any other exit code — non-blocking error; the tool call proceeds.
updated_input, the last non-empty value wins.Crush also accepts the Claude Code hook output format, so existing hooks work unchanged:
{
"hookSpecificOutput": {
"permissionDecision": "allow",
"permissionDecisionReason": "Auto-approved",
"updatedInput": { "command": "echo rewritten" }
}
}
Skills can be invoked as commands. Add user-invocable: true to the skill's
YAML frontmatter:
---
name: my-skill
description: A skill that can be invoked as a command.
user-invocable: true
---
user:skill-name; project skills as
project:skill-name.disable-model-invocation: true to keep a skill user-only (hidden from
the model's available-skills list but still manually invocable).CRUSH_VERSION — exported into crushrc at load; the running version (or
devel for local builds).CRUSH_GLOBAL_CONFIG — override global config location.CRUSH_GLOBAL_DATA — override data directory location.CRUSH_SKILLS_DIR — override default skills directory.crush.json is the original static format. It still works and merges with
crushrc. Basic structure:
{
"$schema": "https://charm.land/crush.json",
"models": {},
"providers": {},
"mcp": {},
"lsp": {},
"hooks": {},
"options": {},
"permissions": {}
}
The $schema property enables IDE autocomplete but is optional.
| crushrc | crush.json |
|---|---|
provider add openai --api-key "$K" | providers.openai = {"api_key": "$K"} |
model add openai/gpt-x --name X | append to providers.openai.models[] |
model large openai/gpt-x | models.large = {"provider":"openai","model":"gpt-x"} |
mcp add gh --type http --url U | mcp.gh = {"type":"http","url":"U"} |
lsp add go --command gopls | lsp.go = {"command":"gopls"} |
hook add PreToolUse --command C | append to hooks.PreToolUse[] |
permissions allow view ls | permissions.allowed_tools = ["view","ls"] |
permissions deny bash | options.disabled_tools = ["bash"] |
option skill-path ./skills | options.skills_paths = ["./skills"] |
option metrics false | options.disable_metrics = true |
option attribution-trailer-style none | options.attribution.trailer_style = "none" |
option attribution-generated-with false | options.attribution.generated_with = false |
In JSON, only selected string fields are run through the embedded shell at load
time (in crushrc, everything is native Bash so this table does not apply):
| Surface | Expansion |
|---|---|
Provider api_key, base_url, api_endpoint, extra_headers | yes |
Provider extra_body | no (JSON passthrough) |
MCP command, args, env, headers, url | yes |
LSP command, args, env | yes |
Hook command | runs via sh -c, not the resolver |
Supported constructs: $VAR, ${VAR}, ${VAR:-default}, ${VAR:+alt},
${VAR:?message}, $(command). An unset variable expands to empty; a failing
$(command) is a hard error. A header that resolves to empty is dropped from
the request.
Both formats are trusted code. crushrc runs entirely, and any $(...) in
crush.json runs at load time, with the invoking user's shell privileges,
before the UI appears. Don't launch Crush in a directory whose config you
haven't reviewed.