SKILL.md

  1---
  2name: crush-config
  3description: Use when the user needs help configuring Crush — working with crush.json, setting up providers, configuring LSPs, adding MCP servers, managing skills or permissions, or changing Crush behavior.
  4---
  5
  6# Crush Configuration
  7
  8Crush uses JSON configuration files with the following priority (highest to lowest):
  9
 101. `.crush.json` (project-local, hidden)
 112. `crush.json` (project-local)
 123. `$XDG_CONFIG_HOME/crush/crush.json` or `$HOME/.config/crush/crush.json` (global)
 13
 14## Basic Structure
 15
 16```json
 17{
 18  "$schema": "https://charm.land/crush.json",
 19  "models": {},
 20  "providers": {},
 21  "mcp": {},
 22  "lsp": {},
 23  "hooks": {},
 24  "options": {},
 25  "permissions": {},
 26  "tools": {}
 27}
 28```
 29
 30The `$schema` property enables IDE autocomplete but is optional.
 31
 32## Shell Expansion
 33
 34Crush runs selected string fields through an embedded bash-compatible
 35shell at load time, so values can pull from env vars, files, or helper
 36commands.
 37
 38Supported constructs (match the `bash` tool):
 39
 40- `$VAR` and `${VAR}`
 41- `${VAR:-default}`, `${VAR:+alt}`, `${VAR:?message}`
 42- `$(command)` with full quoting and nesting
 43- Single- and double-quoted strings, escapes
 44
 45Default semantics match bash: an unset variable expands to an empty
 46string, no error. A failing `$(command)` is always a hard error. For
 47required credentials, use `${VAR:?message}` so a missing variable
 48fails loudly at load time with your message.
 49
 50```json
 51{ "api_key": "${CODEBERG_TOKEN:?set CODEBERG_TOKEN}" }
 52```
 53
 54### Which fields expand
 55
 56| Surface                                             | Expansion |
 57| --------------------------------------------------- | --------- |
 58| Provider `api_key`, `base_url`, `api_endpoint`      | yes       |
 59| Provider `extra_headers`                            | yes       |
 60| Provider `extra_body`                               | **no**    |
 61| MCP `command`, `args`, `env`, `headers`, `url`      | yes       |
 62| LSP `command`, `args`, `env`                        | yes       |
 63| Hook `command`                                      | runs via `sh -c`, not the resolver |
 64
 65`extra_body` is a JSON passthrough. If you need env-driven values in
 66a request body, put them in `extra_headers`, `api_key`, or
 67`base_url` instead.
 68
 69### Empty-resolved headers are dropped
 70
 71When a header value resolves to the empty string (unset variable,
 72`$(echo)`, or literal `""`), the header is omitted from the
 73outgoing request. This keeps optional env-gated headers like
 74`"OpenAI-Organization": "$OPENAI_ORG_ID"` working cleanly when the
 75var isn't set. Applies to MCP `headers` and provider `extra_headers`.
 76
 77### Security note
 78
 79`crush.json` is trusted code. Any `$(...)` in it runs at load time
 80with the invoking user's shell privileges, before the UI appears.
 81Don't launch Crush in a directory whose `crush.json` you haven't
 82reviewed.
 83
 84## Common Tasks
 85
 86- Add a custom provider: add an entry under `providers` with `type`, `base_url`, `api_key`, and `models`.
 87- Disable a builtin or local skill: add the skill name to `options.disabled_skills`.
 88- Add an MCP server: add an entry under `mcp` with `type` and either `command` (stdio) or `url` (http/sse).
 89
 90## Model Selection
 91
 92```json
 93{
 94  "models": {
 95    "large": {
 96      "model": "claude-sonnet-4-20250514",
 97      "provider": "anthropic",
 98      "max_tokens": 16384
 99    },
100    "small": {
101      "model": "claude-haiku-4-20250514",
102      "provider": "anthropic"
103    }
104  }
105}
106```
107
108- `large` is the primary coding model; `small` is for summarization.
109- Only `model` and `provider` are required.
110- Optional tuning: `reasoning_effort`, `think`, `max_tokens`, `temperature`, `top_p`, `top_k`, `frequency_penalty`, `presence_penalty`, `provider_options`.
111
112## Custom Providers
113
114```json
115{
116  "providers": {
117    "deepseek": {
118      "type": "openai-compat",
119      "base_url": "https://api.deepseek.com/v1",
120      "api_key": "$DEEPSEEK_API_KEY",
121      "models": [
122        {
123          "id": "deepseek-chat",
124          "name": "Deepseek V3",
125          "context_window": 64000
126        }
127      ]
128    }
129  }
130}
131```
132
133- `type` (required): `openai`, `openai-compat`, or `anthropic`
134- `api_key`, `base_url`, `api_endpoint`, and `extra_headers` are shell-expanded (see [Shell Expansion](#shell-expansion)).
135- `extra_body` is a JSON passthrough and is **not** expanded.
136- Additional fields: `disable`, `system_prompt_prefix`, `extra_headers`, `extra_body`, `provider_options`.
137
138## LSP Configuration
139
140```json
141{
142  "lsp": {
143    "go": {
144      "command": "gopls",
145      "env": { "GOPATH": "$HOME/go" }
146    },
147    "typescript": {
148      "command": "typescript-language-server",
149      "args": ["--stdio"]
150    }
151  }
152}
153```
154
155- `command` (required), `args`, `env` cover most setups.
156- `command`, `args`, and `env` values are shell-expanded (see [Shell Expansion](#shell-expansion)).
157- Additional fields: `disabled`, `filetypes`, `root_markers`, `init_options`, `options`, `timeout`.
158
159## MCP Servers
160
161```json
162{
163  "mcp": {
164    "filesystem": {
165      "type": "stdio",
166      "command": "node",
167      "args": ["/path/to/mcp-server.js"]
168    },
169    "github": {
170      "type": "http",
171      "url": "https://api.githubcopilot.com/mcp/",
172      "headers": {
173        "Authorization": "Bearer $GH_PAT"
174      }
175    }
176  }
177}
178```
179
180- `type` (required): `stdio`, `sse`, or `http`
181- `command`, `args`, `env`, `headers`, and `url` are shell-expanded (see [Shell Expansion](#shell-expansion)).
182- Additional fields: `env`, `disabled`, `disabled_tools`, `timeout`.
183
184## Options
185
186```json
187{
188  "options": {
189    "skills_paths": ["./skills"],
190    "disabled_tools": ["bash", "sourcegraph"],
191    "disabled_skills": ["crush-config"],
192    "tui": {
193      "compact_mode": false,
194      "diff_mode": "unified",
195      "transparent": false
196    },
197    "auto_lsp": true,
198    "debug": false,
199    "debug_lsp": false,
200    "attribution": {
201      "trailer_style": "assisted-by",
202      "generated_with": true
203    }
204  }
205}
206```
207
208> [!IMPORTANT]
209> The following skill paths are loaded by default and DO NOT NEED to be added to `skills_paths`:
210> `.agents/skills`, `.crush/skills`, `.claude/skills`, `.cursor/skills`
211
212Other options: `context_paths`, `progress`, `disable_notifications`, `disable_auto_summarize`, `disable_metrics`, `disable_provider_auto_update`, `disable_default_providers`, `data_directory`, `initialize_as`.
213
214## User-Invocable Skills
215
216Skills can be made invocable as commands from the commands palette. Add `user-invocable: true` to the skill's YAML frontmatter:
217
218```yaml
219---
220name: my-skill
221description: A skill that can be invoked as a command.
222user-invocable: true
223---
224```
225
226User-invocable skills appear in the commands palette with a prefix:
227- Skills from global directories: `user:skill-name`
228- Skills from project directories: `project:skill-name`
229
230When invoked, the skill's instructions are loaded into the conversation context.
231
232To prevent the model from auto-triggering a skill (while still allowing user invocation), add `disable-model-invocation: true`:
233
234```yaml
235---
236name: my-skill
237description: Only invocable by users, not the model.
238user-invocable: true
239disable-model-invocation: true
240---
241```
242
243Skills with `disable-model-invocation` won't appear in the model's available skills list but can still be invoked manually by users.
244
245## Hooks
246
247Hooks are user-defined shell commands that fire on agent events. Currently only `PreToolUse` is supported, which runs before a tool is executed.
248
249```json
250{
251  "hooks": {
252    "PreToolUse": [
253      {
254        "matcher": "^(edit|write|multiedit)$",
255        "command": ".crush/hooks/protect-files.sh"
256      },
257      {
258        "matcher": "^bash$",
259        "command": ".crush/hooks/no-haskell.sh"
260      }
261    ]
262  }
263}
264```
265
266### Hook Properties
267
268- `command` (required): Shell command to execute. Runs via `sh -c`.
269- `matcher` (optional): Regex pattern tested against the tool name. Empty or absent means match all tools.
270- `timeout` (optional): Timeout in seconds. Defaults to 30.
271
272### Event Name Normalization
273
274Event names are case-insensitive and accept snake_case variants: `PreToolUse`, `pretooluse`, `pre_tool_use`, and `PRE_TOOL_USE` all work.
275
276### How Hooks Work
277
2781. When a tool is about to be called, all `PreToolUse` hooks with a matching `matcher` (or no matcher) run in parallel.
2792. Duplicate commands are deduplicated — each unique command runs at most once.
2803. The hook receives JSON on **stdin** and hook-specific **environment variables**.
281
282### Hook Input (stdin)
283
284A JSON payload is piped to the hook command:
285
286```json
287{
288  "event": "PreToolUse",
289  "session_id": "abc-123",
290  "cwd": "/path/to/project",
291  "tool_name": "bash",
292  "tool_input": {"command": "ls -la"}
293}
294```
295
296### Hook Environment Variables
297
298| Variable | Description |
299|---|---|
300| `CRUSH_EVENT` | Event name (e.g. `PreToolUse`) |
301| `CRUSH_TOOL_NAME` | Name of the tool being called |
302| `CRUSH_SESSION_ID` | Current session ID |
303| `CRUSH_CWD` | Current working directory |
304| `CRUSH_PROJECT_DIR` | Project root directory |
305| `CRUSH_TOOL_INPUT_COMMAND` | Value of `command` from tool input (if present) |
306| `CRUSH_TOOL_INPUT_FILE_PATH` | Value of `file_path` from tool input (if present) |
307
308### Hook Output
309
310**Exit code 0** — the hook succeeded. Stdout is parsed as JSON:
311
312```json
313{"decision": "allow", "context": "optional context appended to tool result"}
314```
315
316- `decision`: `allow` to explicitly allow, `deny` to block, `none` (or omit) for no opinion.
317- `reason`: Explanation text (used when denying).
318- `context`: Extra context appended to the tool result.
319- `updated_input`: Replacement JSON for the tool input. Last non-empty value wins.
320
321**Exit code 2** — the tool call is blocked. Stderr is used as the deny reason.
322
323```bash
324echo "No Haskell allowed" >&2
325exit 2
326```
327
328**Any other exit code** — non-blocking error. The tool call proceeds as normal.
329
330### Claude Code Compatibility
331
332Crush also supports the Claude Code hook output format:
333
334```json
335{
336  "hookSpecificOutput": {
337    "permissionDecision": "allow",
338    "permissionDecisionReason": "Auto-approved",
339    "updatedInput": {"command": "echo rewritten"}
340  }
341}
342```
343
344Existing Claude Code hooks should work without modification.
345
346### Decision Aggregation
347
348When multiple hooks match, their decisions are aggregated:
349
350- **Deny wins over allow** — if any hook denies, the tool call is blocked.
351- **Allow wins over none** — if no hook denies but at least one allows, the call proceeds.
352- All deny reasons are concatenated (newline-separated).
353- All context strings are concatenated (newline-separated).
354- For `updated_input`, the last non-empty value wins.
355
356## Tool Permissions
357
358```json
359{
360  "permissions": {
361    "allowed_tools": ["view", "ls", "grep", "edit"]
362  }
363}
364```
365
366## Environment Variables
367
368- `CRUSH_GLOBAL_CONFIG` - Override global config location
369- `CRUSH_GLOBAL_DATA` - Override data directory location
370- `CRUSH_SKILLS_DIR` - Override default skills directory