| name | crush-config |
| description | 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. |
Crush Configuration
Crush uses JSON configuration files with the following priority (highest to lowest):
.crush.json (project-local, hidden)
crush.json (project-local)
$XDG_CONFIG_HOME/crush/crush.json or $HOME/.config/crush/crush.json (global)
Basic Structure
{
"$schema": "https://charm.land/crush.json",
"models": {},
"providers": {},
"mcp": {},
"lsp": {},
"hooks": {},
"options": {},
"permissions": {},
"tools": {}
}
The $schema property enables IDE autocomplete but is optional.
Shell Expansion
Crush runs selected string fields through an embedded bash-compatible
shell at load time, so values can pull from env vars, files, or helper
commands.
Supported constructs (match the bash tool):
$VAR and ${VAR}
${VAR:-default}, ${VAR:+alt}, ${VAR:?message}
$(command) with full quoting and nesting
- Single- and double-quoted strings, escapes
Default semantics match bash: an unset variable expands to an empty
string, no error. A failing $(command) is always a hard error. For
required credentials, use ${VAR:?message} so a missing variable
fails loudly at load time with your message.
{ "api_key": "${CODEBERG_TOKEN:?set CODEBERG_TOKEN}" }
Which fields expand
| Surface | Expansion |
|---|
Provider api_key, base_url, api_endpoint | yes |
Provider extra_headers | yes |
Provider extra_body | no |
MCP command, args, env, headers, url | yes |
LSP command, args, env | yes |
Hook command | runs via sh -c, not the resolver |
extra_body is a JSON passthrough. If you need env-driven values in
a request body, put them in extra_headers, api_key, or
base_url instead.
Empty-resolved headers are dropped
When a header value resolves to the empty string (unset variable,
$(echo), or literal ""), the header is omitted from the
outgoing request. This keeps optional env-gated headers like
"OpenAI-Organization": "$OPENAI_ORG_ID" working cleanly when the
var isn't set. Applies to MCP headers and provider extra_headers.
Security note
crush.json is trusted code. Any $(...) in it runs at load time
with the invoking user's shell privileges, before the UI appears.
Don't launch Crush in a directory whose crush.json you haven't
reviewed.
Common Tasks
- Add a custom provider: add an entry under
providers with type, base_url, api_key, and models.
- Disable a builtin or local skill: add the skill name to
options.disabled_skills.
- Add an MCP server: add an entry under
mcp with type and either command (stdio) or url (http/sse).
Model Selection
{
"models": {
"large": {
"model": "claude-sonnet-4-20250514",
"provider": "anthropic",
"max_tokens": 16384
},
"small": {
"model": "claude-haiku-4-20250514",
"provider": "anthropic"
}
}
}
large is the primary coding model; small is for summarization.
- Only
model and provider are required.
- Optional tuning:
reasoning_effort, think, max_tokens, temperature, top_p, top_k, frequency_penalty, presence_penalty, provider_options.
Custom Providers
{
"providers": {
"deepseek": {
"type": "openai-compat",
"base_url": "https://api.deepseek.com/v1",
"api_key": "$DEEPSEEK_API_KEY",
"models": [
{
"id": "deepseek-chat",
"name": "Deepseek V3",
"context_window": 64000
}
]
}
}
}
type (required): openai, openai-compat, anthropic, or a local provider type (llamacpp, omlx, lmstudio, litellm, ollama)
api_key, base_url, api_endpoint, and extra_headers are shell-expanded (see Shell Expansion).
extra_body is a JSON passthrough and is not expanded.
- Additional fields:
disable, system_prompt_prefix, extra_headers, extra_body, provider_options.
LSP Configuration
{
"lsp": {
"go": {
"command": "gopls",
"env": { "GOPATH": "$HOME/go" }
},
"typescript": {
"command": "typescript-language-server",
"args": ["--stdio"]
}
}
}
command (required), args, env cover most setups.
command, args, and env values are shell-expanded (see Shell Expansion).
- Additional fields:
disabled, filetypes, root_markers, init_options, options, timeout.
MCP Servers
{
"mcp": {
"filesystem": {
"type": "stdio",
"command": "node",
"args": ["/path/to/mcp-server.js"]
},
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": "Bearer $GH_PAT"
}
}
}
}
type (required): stdio, sse, or http
command, args, env, headers, and url are shell-expanded (see Shell Expansion).
- Additional fields:
env, disabled, disabled_tools, timeout.
Options
{
"options": {
"skills_paths": ["./skills"],
"disabled_tools": ["bash", "sourcegraph"],
"disabled_skills": ["crush-config"],
"tui": {
"compact_mode": false,
"diff_mode": "unified",
"transparent": false
},
"auto_lsp": true,
"debug": false,
"debug_lsp": false,
"attribution": {
"trailer_style": "assisted-by",
"generated_with": true
}
}
}
[!IMPORTANT]
The following skill paths are loaded by default and DO NOT NEED to be added to skills_paths:
.agents/skills, .crush/skills, .claude/skills, .cursor/skills
Other options: context_paths, progress, disable_notifications, disable_auto_summarize, disable_metrics, disable_provider_auto_update, disable_default_providers, data_directory, initialize_as.
User-Invocable Skills
Skills can be made invocable as commands from the commands palette. 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-invocable skills appear in the commands palette with a prefix:
- Skills from global directories:
user:skill-name
- Skills from project directories:
project:skill-name
When invoked, the skill's instructions are loaded into the conversation context.
To prevent the model from auto-triggering a skill (while still allowing user invocation), add disable-model-invocation: true:
---
name: my-skill
description: Only invocable by users, not the model.
user-invocable: true
disable-model-invocation: true
---
Skills with disable-model-invocation won't appear in the model's available skills list but can still be invoked manually by users.
Hooks
Hooks are user-defined shell commands that fire on agent events. Currently only PreToolUse is supported, which runs before a tool is executed.
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(edit|write|multiedit)$",
"command": ".crush/hooks/protect-files.sh"
},
{
"matcher": "^bash$",
"command": ".crush/hooks/no-haskell.sh"
}
]
}
}
Hook Properties
command (required): Shell command to execute. Runs via sh -c.
matcher (optional): Regex pattern tested against the tool name. Empty or absent means match all tools.
timeout (optional): Timeout in seconds. Defaults to 30.
Event Name Normalization
Event names are case-insensitive and accept snake_case variants: PreToolUse, pretooluse, pre_tool_use, and PRE_TOOL_USE all work.
How Hooks Work
- When a tool is about to be called, all
PreToolUse hooks with a matching matcher (or no matcher) run in parallel.
- Duplicate commands are deduplicated — each unique command runs at most once.
- The hook receives JSON on stdin and hook-specific environment variables.
Hook Input (stdin)
A JSON payload is piped to the hook command:
{
"event": "PreToolUse",
"session_id": "abc-123",
"cwd": "/path/to/project",
"tool_name": "bash",
"tool_input": {"command": "ls -la"}
}
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 |
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 — the 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) for no opinion.
reason: Explanation text (used when denying).
context: Extra context appended to the tool result.
updated_input: Replacement JSON for the tool input. Last non-empty value wins.
Exit code 2 — the tool call is blocked. Stderr is used as the deny reason.
echo "No Haskell allowed" >&2
exit 2
Any other exit code — non-blocking error. The tool call proceeds as normal.
Claude Code Compatibility
Crush also supports the Claude Code hook output format:
{
"hookSpecificOutput": {
"permissionDecision": "allow",
"permissionDecisionReason": "Auto-approved",
"updatedInput": {"command": "echo rewritten"}
}
}
Existing Claude Code hooks should work without modification.
Decision Aggregation
When multiple hooks match, their decisions are aggregated:
- Deny wins over allow — if any hook denies, the tool call is blocked.
- Allow wins over none — if no hook denies but at least one allows, the call proceeds.
- All deny reasons are concatenated (newline-separated).
- All context strings are concatenated (newline-separated).
- For
updated_input, the last non-empty value wins.
Tool Permissions
{
"permissions": {
"allowed_tools": ["view", "ls", "grep", "edit"]
}
}
Environment Variables
CRUSH_GLOBAL_CONFIG - Override global config location
CRUSH_GLOBAL_DATA - Override data directory location
CRUSH_SKILLS_DIR - Override default skills directory