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## Hooks
215
216Hooks are user-defined shell commands that fire on agent events. Currently only `PreToolUse` is supported, which runs before a tool is executed.
217
218```json
219{
220 "hooks": {
221 "PreToolUse": [
222 {
223 "matcher": "^(edit|write|multiedit)$",
224 "command": ".crush/hooks/protect-files.sh"
225 },
226 {
227 "matcher": "^bash$",
228 "command": ".crush/hooks/no-haskell.sh"
229 }
230 ]
231 }
232}
233```
234
235### Hook Properties
236
237- `command` (required): Shell command to execute. Runs via `sh -c`.
238- `matcher` (optional): Regex pattern tested against the tool name. Empty or absent means match all tools.
239- `timeout` (optional): Timeout in seconds. Defaults to 30.
240
241### Event Name Normalization
242
243Event names are case-insensitive and accept snake_case variants: `PreToolUse`, `pretooluse`, `pre_tool_use`, and `PRE_TOOL_USE` all work.
244
245### How Hooks Work
246
2471. When a tool is about to be called, all `PreToolUse` hooks with a matching `matcher` (or no matcher) run in parallel.
2482. Duplicate commands are deduplicated — each unique command runs at most once.
2493. The hook receives JSON on **stdin** and hook-specific **environment variables**.
250
251### Hook Input (stdin)
252
253A JSON payload is piped to the hook command:
254
255```json
256{
257 "event": "PreToolUse",
258 "session_id": "abc-123",
259 "cwd": "/path/to/project",
260 "tool_name": "bash",
261 "tool_input": {"command": "ls -la"}
262}
263```
264
265### Hook Environment Variables
266
267| Variable | Description |
268|---|---|
269| `CRUSH_EVENT` | Event name (e.g. `PreToolUse`) |
270| `CRUSH_TOOL_NAME` | Name of the tool being called |
271| `CRUSH_SESSION_ID` | Current session ID |
272| `CRUSH_CWD` | Current working directory |
273| `CRUSH_PROJECT_DIR` | Project root directory |
274| `CRUSH_TOOL_INPUT_COMMAND` | Value of `command` from tool input (if present) |
275| `CRUSH_TOOL_INPUT_FILE_PATH` | Value of `file_path` from tool input (if present) |
276
277### Hook Output
278
279**Exit code 0** — the hook succeeded. Stdout is parsed as JSON:
280
281```json
282{"decision": "allow", "context": "optional context appended to tool result"}
283```
284
285- `decision`: `allow` to explicitly allow, `deny` to block, `none` (or omit) for no opinion.
286- `reason`: Explanation text (used when denying).
287- `context`: Extra context appended to the tool result.
288- `updated_input`: Replacement JSON for the tool input. Last non-empty value wins.
289
290**Exit code 2** — the tool call is blocked. Stderr is used as the deny reason.
291
292```bash
293echo "No Haskell allowed" >&2
294exit 2
295```
296
297**Any other exit code** — non-blocking error. The tool call proceeds as normal.
298
299### Claude Code Compatibility
300
301Crush also supports the Claude Code hook output format:
302
303```json
304{
305 "hookSpecificOutput": {
306 "permissionDecision": "allow",
307 "permissionDecisionReason": "Auto-approved",
308 "updatedInput": {"command": "echo rewritten"}
309 }
310}
311```
312
313Existing Claude Code hooks should work without modification.
314
315### Decision Aggregation
316
317When multiple hooks match, their decisions are aggregated:
318
319- **Deny wins over allow** — if any hook denies, the tool call is blocked.
320- **Allow wins over none** — if no hook denies but at least one allows, the call proceeds.
321- All deny reasons are concatenated (newline-separated).
322- All context strings are concatenated (newline-separated).
323- For `updated_input`, the last non-empty value wins.
324
325## Tool Permissions
326
327```json
328{
329 "permissions": {
330 "allowed_tools": ["view", "ls", "grep", "edit"]
331 }
332}
333```
334
335## Environment Variables
336
337- `CRUSH_GLOBAL_CONFIG` - Override global config location
338- `CRUSH_GLOBAL_DATA` - Override data directory location
339- `CRUSH_SKILLS_DIR` - Override default skills directory