internal/skills/builtin/crush-config/SKILL.md
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){
"$schema": "https://charm.land/crush.json",
"models": {},
"providers": {},
"mcp": {},
"lsp": {},
"hooks": {},
"options": {},
"permissions": {},
"tools": {}
}
The $schema property enables IDE autocomplete but is optional.
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 nestingDefault 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}" }
| 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.
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.
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.
providers with type, base_url, api_key, and models.options.disabled_skills.mcp with type and either command (stdio) or url (http/sse).{
"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.model and provider are required.reasoning_effort, think, max_tokens, temperature, top_p, top_k, frequency_penalty, presence_penalty, provider_options.{
"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, or anthropicapi_key, base_url, api_endpoint, and extra_headers are shell-expanded (see Shell Expansion).extra_body is a JSON passthrough and is not expanded.disable, system_prompt_prefix, extra_headers, extra_body, provider_options.{
"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).disabled, filetypes, root_markers, init_options, options, timeout.{
"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 httpcommand, args, env, headers, and url are shell-expanded (see Shell Expansion).env, disabled, disabled_tools, timeout.{
"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.
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"
}
]
}
}
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 names are case-insensitive and accept snake_case variants: PreToolUse, pretooluse, pre_tool_use, and PRE_TOOL_USE all work.
PreToolUse hooks with a matching matcher (or no matcher) run in parallel.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"}
}
| 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 — 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.
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.
When multiple hooks match, their decisions are aggregated:
updated_input, the last non-empty value wins.{
"permissions": {
"allowed_tools": ["view", "ls", "grep", "edit"]
}
}
CRUSH_GLOBAL_CONFIG - Override global config locationCRUSH_GLOBAL_DATA - Override data directory locationCRUSH_SKILLS_DIR - Override default skills directory