--- name: crush-config description: Use when the user needs help configuring Crush — writing crushrc (the Bash config format) or crush.json, setting up providers, models, LSPs, MCP servers, hooks, skills, permissions, or changing Crush behavior. --- # Crush Configuration 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](#legacy-json-format). Both are discovered together and deep-merged. Priority (highest to lowest): 1. `.crushrc` / `crushrc` / `.crush.json` / `crush.json` (project-local, closer-to-cwd wins; Windows uses `.\.crushrc` / `.\crushrc`) 2. `$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. ## crushrc at a glance 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`. ```bash #!/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): ```bash [[ "$CRUSH_VERSION" != devel ]] && lsp add gopls --command gopls ``` ## Commands All entity commands are verb-first. `remove` accepts `rm` as an alias. Booleans accept `true/false/1/0/yes/no`, case-insensitive. ### providers ```bash provider add [flags] # define/update; repeated calls merge provider remove # 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`. ```bash provider add deepseek \ --type openai-compat \ --base-url "https://api.deepseek.com/v1" \ --api-key "${DEEPSEEK_API_KEY:?set DEEPSEEK_API_KEY}" ``` ### models ```bash model add / [flags] # register a custom model (provider must exist) model remove / # alias: rm model large [/] [flags] # set the large slot; no arg prints it model small [/] [flags] # set the small slot; no arg prints it ``` - `/` 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 ```bash mcp add --type stdio|sse|http [flags] # default type is stdio mcp remove # 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), `--oauth BOOL`, `--oauth-client-id ID`, `--oauth-client-secret SECRET`, `--oauth-callback-port PORT`. ```bash 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 ```bash lsp add --command CMD [flags] lsp remove # 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`. ```bash lsp add go --command gopls --env GOPATH "$HOME/go" lsp add typescript --command typescript-language-server --args --stdio ``` ### hooks ```bash hook add --command CMD [--name NAME] [--matcher REGEX] [--timeout N] hook remove [--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](#hooks-runtime) for how hooks execute (stdin payload, env vars, decisions). ```bash hook add PreToolUse --matcher "^bash$" --command ".crush/hooks/no-haskell.sh" --name no-haskell ``` ### permissions ```bash permissions allow [ ...] # tools that skip permission prompts permissions deny [ ...] # 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. ### options ```bash option [value] option reset # clear a list option back to empty ``` - **Boolean keys** (value optional, defaults `true`): `debug`, `debug-lsp`, `auto-lsp`, `progress`. - **Boolean keys phrased positively** (stored as the negated field): `metrics`, `auto-summarize`, `provider-auto-update`, `default-providers`. Example: `option metrics false` disables metrics. - **String keys**: `data-directory`, `initialize-as`, `notifications`. - **Attribution keys**: `attribution-trailer-style` (`none`, `co-authored-by`, `assisted-by`) and `attribution-generated-with` (boolean). - **UI settings**: `option ui compact BOOL`, `option ui diff unified|split`, `option ui transparent BOOL`, `option ui mouse BOOL` (default `true`; disable to let the terminal/tmux handle selection, copy/paste, and scrolling), `option ui scrollbar default|always|never`, `option ui exit-banner default|compact|none`, `option ui completions-max-depth N`, `option ui completions-max-items N`. - **List keys** (singular, one value per call, repeatable): `context-path`, `global-context-path`, `skill-path`, `disable-skill`. Use `option reset ` to wipe inherited values (e.g. after `source`). ```bash 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 option ui exit-banner compact ``` > [!IMPORTANT] These skill paths are loaded by default and do NOT need > `skill-path`: `.agents/skills`, `.crush/skills`, `.claude/skills`, > `.cursor/skills`. ## Hooks runtime Hooks are user-defined shell commands that fire on agent events. Crush supports 13 events: `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionResult`, `SessionStart`, `SessionEnd`, `TurnStart`, `TurnEnd`, `StopFailure`, `Interrupt`, `PreCompact`, and `PostCompact`. `--matcher` is only accepted on the first five. This behavior is the same however the hook is defined (`hook add` or JSON). ### How hooks work 1. When a tool is about to be called, all `PreToolUse` hooks with a matching `matcher` (or no matcher) run in parallel. 2. Duplicate commands are deduplicated — each unique command runs at most once. 3. The hook receives JSON on **stdin** and hook-specific **environment variables**. Event names are case-insensitive and accept snake_case: `PreToolUse`, `pretooluse`, `pre_tool_use`, `PRE_TOOL_USE` all work. ### Hook input (stdin) ```json { "event": "PreToolUse", "hook_event_name": "PreToolUse", "session_id": "abc-123", "cwd": "/path/to/project", "tool_name": "bash", "tool_input": { "command": "ls -la" }, "tool_use_id": "call_01" } ``` Events without a tool call omit the `tool_*` fields. Some add one of their own: `tool_response` (PostToolUse), `error` (PostToolUseFailure, StopFailure), `last_assistant_message` (TurnEnd), `decision` (PermissionResult), `source` (SessionStart), `reason` (SessionEnd), and `trigger` (PreCompact, PostCompact). ### Hook environment variables | 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 | | `CLAUDE_PROJECT_DIR` | Same value, for scripts written for Claude Code | | `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) | ### Hook output **Exit code 0** — hook succeeded. Stdout is parsed as JSON: ```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`: on PreToolUse, a shallow-merge patch against the tool input. - `updated_output`: on PostToolUse, a string that replaces the tool output. - `halt`: `true` ends the turn (blocks compaction on PreCompact). **Exit code 2** — the tool call is blocked; stderr is the deny reason. **Exit code 49** — the whole turn is halted; stderr is the reason. **Any other exit code** — non-blocking error; the tool call proceeds, unless stdout holds a JSON object, which is applied anyway. ### Decision aggregation - **Deny wins over allow** — any deny blocks the call. - **Allow wins over none** — a lone allow lets it proceed. - Deny reasons and context strings are concatenated (newline-separated). - `updated_input` patches shallow-merge in config order on PreToolUse; `updated_output` on PostToolUse is last-one-wins. ### Claude Code compatibility Crush also accepts the Claude Code hook output formats (`hookSpecificOutput`, top-level `decision: "block"` with `reason`, `continue: false`), so existing hooks work unchanged: ```json { "hookSpecificOutput": { "permissionDecision": "allow", "permissionDecisionReason": "Auto-approved", "updatedInput": { "command": "echo rewritten" } } } ``` ## User-invocable skills Skills can be invoked as commands. Add `user-invocable: true` to the skill's YAML frontmatter: ```yaml --- name: my-skill description: A skill that can be invoked as a command. user-invocable: true --- ``` - Global skills appear as `user:skill-name`; project skills as `project:skill-name`. - Add `disable-model-invocation: true` to keep a skill user-only (hidden from the model's available-skills list but still manually invocable). ## Environment variables - `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. ## Legacy JSON format `crush.json` is the original static format. It still works and merges with `crushrc`. Basic structure: ```json { "$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 mapping | 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 request-timeout 300` | `options.request_timeout = 300` | | `option max-background-jobs 200` | `options.max_background_jobs = 200` | | `option attribution-trailer-style none` | `options.attribution.trailer_style = "none"` | | `option attribution-generated-with false` | `options.attribution.generated_with = false` | ### Shell expansion in crush.json 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. ### Security note 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.