{"owner":"charmbracelet","repo":"crush","hasSkills":true,"totalSkillsCount":6,"totalTokensCount":10149,"categories":["anthropic-skill","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/builtin-skills/SKILL.md",".agents/skills/shell-builtins/SKILL.md","AGENTS.md","internal/skills/builtin/crush-config/SKILL.md","internal/skills/builtin/crush-hooks/SKILL.md","internal/skills/builtin/jq/SKILL.md"],"skills":{".agents/skills/builtin-skills/SKILL.md":"---\nname: builtin-skills\ndescription:\n  Use when creating a new builtin skill for Crush, editing an existing builtin\n  skill (internal/skills/builtin/), or when the user needs to understand how the\n  embedded skill system works.\n---\n\n# Builtin Skills\n\nCrush embeds skills directly into the binary via `internal/skills/builtin/`.\nThese are always available without user configuration.\n\n## How It Works\n\n- Each skill lives in `internal/skills/builtin/<skill-name>/SKILL.md`.\n- The tree is embedded at compile time via `//go:embed builtin/*` in\n  `internal/skills/embed.go`.\n- `DiscoverBuiltin()` walks the embedded FS, parses each `SKILL.md`, and sets\n  paths with the `crush://skills/` prefix (e.g., `crush://skills/jq/SKILL.md`).\n- The View tool resolves `crush://` paths from the embedded FS, not disk.\n- User skills with the same name override builtins (last occurrence wins in\n  `Deduplicate()`).\n\n## Adding a New Builtin Skill\n\n1. Create `internal/skills/builtin/<skill-name>/SKILL.md` with YAML frontmatter\n   (`name`, `description`) and markdown instructions. The directory name must\n   match the `name` field.\n2. No extra wiring needed — `//go:embed builtin/*` picks up new directories\n   automatically.\n3. Add a test assertion in `TestDiscoverBuiltin` in\n   `internal/skills/skills_test.go` to verify discovery.\n4. Build and test: `go build . && go test ./internal/skills/...`\n\n## Existing Builtin Skills\n\n| Skill          | Directory               | Description                                |\n| -------------- | ----------------------- | ------------------------------------------ |\n| `crush-config` | `builtin/crush-config/` | Crush configuration help                   |\n| `crush-hooks`  | `builtin/crush-hooks/`  | Authoring, configuring and debugging hooks |\n| `jq`           | `builtin/jq/`           | jq JSON processor usage guide              |\n",".agents/skills/shell-builtins/SKILL.md":"---\nname: shell-builtins\ndescription: Use when creating a new shell builtin command for Crush (internal/shell/), editing an existing one, or when the user needs to understand how commands are intercepted in Crush's embedded shell.\n---\n\n# Shell Builtins\n\nCrush's shell (`internal/shell/`) uses `mvdan.cc/sh/v3` for POSIX shell\nemulation. Commands can be intercepted before they reach the OS by adding\n**builtins** — functions handled in-process.\n\n## How Builtins Work\n\nBuiltins live in `builtinHandler()` in `internal/shell/run.go`. This is an\n`interp.ExecHandlerFunc` middleware registered in `standardHandlers()`\n**before** the block handler, so builtins run even for commands that would\notherwise be blocked. The same handler chain is shared by the stateful\n`Shell` type and the stateless `Run` entrypoint used by the hook runner,\nso builtins are available identically in the `bash` tool and in hooks.\n\nThe handler is a switch on `args[0]`. Each case either handles the command\ninline or delegates to a helper function.\n\n## Adding a New Builtin\n\n1. **Add the case** to the switch in `builtinHandler()` in `run.go`.\n2. **Get I/O from the handler context**, not from `os.Stdin`/`os.Stdout`.\n   This ensures the builtin works with pipes and redirections:\n   ```go\n   case \"mycommand\":\n       hc := interp.HandlerCtx(ctx)\n       return handleMyCommand(ctx, args, hc.Stdin, hc.Stdout, hc.Stderr)\n   ```\n3. **Implement the handler** in its own file (e.g.,\n   `internal/shell/mycommand.go`). The function signature must accept a\n   `context.Context` as the first parameter, plus args, stdin, stdout, and\n   stderr:\n   ```go\n   func handleMyCommand(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {\n       // args[0] is the command name (\"mycommand\"), args[1:] are arguments.\n       // Write output to stdout, errors to stderr.\n       // Return nil on success, or interp.ExitStatus(n) for non-zero exit codes.\n   }\n   ```\n4. **Poll `ctx` in every unbounded loop.** Builtins that iterate over\n   input, emit values in a generator-style loop, or do any other work\n   that can exceed a few milliseconds MUST check `ctx.Err()` on each\n   iteration and return it verbatim when non-nil. Hook timeouts rely on\n   this: an unbounded builtin that never polls ctx cannot be interrupted\n   by a hook's `timeout_sec`, and the hook runner will have to abandon\n   the goroutine (see `internal/hooks/runner.go`). Returning `ctx.Err()`\n   (not `interp.ExitStatus(n)`) lets callers distinguish \"command exited\n   non-zero\" from \"we ran out of time\".\n   ```go\n   for _, item := range items {\n       if err := ctx.Err(); err != nil {\n           return err\n       }\n       // ... process item\n   }\n   ```\n5. **Return values**: return `nil` for success, `interp.ExitStatus(n)` for\n   non-zero exit codes, or `ctx.Err()` on cancellation. Write error\n   messages to `stderr` before returning.\n6. **No extra wiring needed** — `builtinHandler()` is already registered\n   in `standardHandlers()`.\n\n## Existing Builtins\n\n| Command | File | Description |\n|---------|------|-------------|\n| `jq` | `jq.go` | JSON processor using `github.com/itchyny/gojq` |\n","AGENTS.md":"# Crush Development Guide\n\n## Project Overview\n\nCrush is a terminal-based AI coding assistant built in Go by\n[Charm](https://charm.land). It connects to LLMs and gives them tools to read,\nwrite, and execute code. It supports multiple providers (Anthropic, OpenAI,\nGemini, Bedrock, Copilot, Hyper, MiniMax, Vercel, and more), integrates with\nLSPs for code intelligence, and supports extensibility via MCP servers and\nagent skills.\n\nThe module path is `github.com/charmbracelet/crush`.\n\n## Architecture\n\n```\nmain.go                            CLI entry point (cobra via internal/cmd)\ninternal/\n  app/app.go                       Top-level wiring: DB, config, agents, LSP, MCP, events\n  cmd/                             CLI commands (root, run, login, models, stats, sessions)\n  config/\n    config.go                      Config struct, context file paths, agent definitions\n    load.go                        crushrc and crush.json loading and validation\n    provider.go                    Provider configuration and model resolution\n  shellconfig/                      Bash-powered config format (crushrc builtins)\n  agent/\n    agent.go                       SessionAgent: runs LLM conversations per session\n    coordinator.go                 Coordinator: manages named agents (\"coder\", \"task\")\n    hooked_tool.go                 Decorator that runs PreToolUse hooks before tool execution\n    prompts.go                     Loads Go-template system prompts\n    templates/                     System prompt templates (coder.md.tpl, task.md.tpl, etc.)\n    tools/                         All built-in tools (bash, edit, view, grep, glob, etc.)\n      mcp/                         MCP client integration\n  hooks/                           Hook engine: runs user shell commands on hook events\n    hooks.go                       Decision types, aggregation logic, event constants\n    runner.go                      Parallel hook execution, timeout, dedup\n    input.go                       Stdin payload builder, env vars, stdout parsing (Crush + Claude Code compat)\n  session/session.go               Session CRUD backed by SQLite\n  message/                         Message model and content types\n  db/                              SQLite via sqlc, with migrations\n    sql/                           Raw SQL queries (consumed by sqlc)\n    migrations/                    Schema migrations\n  lsp/                             LSP client manager, auto-discovery, on-demand startup\n  ui/                              Bubble Tea v2 TUI (see internal/ui/AGENTS.md)\n  permission/                      Tool permission checking and allow-lists\n  skills/                          Skill file discovery and loading\n  shell/                           Bash command execution with background job support\n  event/                           Telemetry (PostHog)\n  pubsub/                          Internal pub/sub for cross-component messaging\n  filetracker/                     Tracks files touched per session\n  history/                         Prompt history\n```\n\n### Key Dependency Roles\n\n- **`charm.land/fantasy`**: LLM provider abstraction layer. Handles protocol\n  differences between Anthropic, OpenAI, Gemini, etc. Used in `internal/app`\n  and `internal/agent`.\n- **`charm.land/bubbletea/v2`**: TUI framework powering the interactive UI.\n- **`charm.land/lipgloss/v2`**: Terminal styling.\n- **`charm.land/glamour/v2`**: Markdown rendering in the terminal.\n- **`charm.land/catwalk`**: Snapshot/golden-file testing for TUI components.\n- **`sqlc`**: Generates Go code from SQL queries in `internal/db/sql/`.\n\n### Key Patterns\n\n- **Config is a Service**: accessed via `config.Service`, not global state.\n- **Tools are self-documenting**: each tool has a `.go` implementation and a\n  `.md` description file in `internal/agent/tools/`.\n- **System prompts are Go templates**: `internal/agent/templates/*.md.tpl`\n  with runtime data injected.\n- **Context files**: Crush reads AGENTS.md, CRUSH.md, CLAUDE.md, GEMINI.md\n  (and `.local` variants) from the working directory for project-specific\n  instructions.\n- **Bash config format**: Crush's primary config format is `crushrc` — a\n  Bash script using builtins (`provider`, `model`, `mcp`, `lsp`,\n  `permissions`, `hook`, `options`) to define config. `crush.json` is still\n  supported but is deprecated in favor of `crushrc` and may be removed in a\n  future release. Shell config files are discovered alongside JSON configs\n  and deep-merged through the same pipeline. Builtins are registered via\n  `shell.RegisterBuiltin` and gated by a `ConfigBuilder` on the context —\n  they are no-ops during normal bash tool execution. See\n  `internal/shellconfig/`.\n- **Persistence**: SQLite + sqlc. All queries live in `internal/db/sql/`,\n  generated code in `internal/db/`. Migrations in `internal/db/migrations/`.\n- **Pub/sub**: `internal/pubsub` for decoupled communication between agent,\n  UI, and services.\n- **Hooks**: User-defined shell commands in `crushrc` (or `crush.json`)\n  that fire before tool execution. The engine (`internal/hooks/`) is\n  independent of fantasy and agent — it takes inputs, runs commands,\n  returns decisions. The `hookedTool` decorator in\n  `internal/agent/hooked_tool.go` wraps tools at the coordinator level.\n  Hooks run before permission checks. See `HOOKS.md` for the user-facing\n  protocol.\n- **CGO disabled**: builds with `CGO_ENABLED=0` and\n  `GOEXPERIMENT=greenteagc`.\n\n## Build/Test/Lint Commands\n\n- **Build**: `go build .` or `go run .`\n- **Test**: `task test` or `go test ./...` (run single test:\n  `go test ./internal/llm/prompt -run TestGetContextFromPaths`)\n- **Update Golden Files**: `go test ./... -update` (regenerates `.golden`\n  files when test output changes)\n  - Update specific package:\n    `go test ./internal/tui/components/core -update` (in this case,\n    we're updating \"core\")\n- **Lint**: `task lint:fix`\n- **Format**: `task fmt` (`gofumpt -w .`)\n- **Modernize**: `task modernize` (runs `modernize` which makes code\n  simplifications)\n- **Dev**: `task dev` (runs with profiling enabled)\n\n## Code Style Guidelines\n\n- **Imports**: Use `goimports` formatting, group stdlib, external, internal\n  packages.\n- **Formatting**: Use gofumpt (stricter than gofmt), enabled in\n  golangci-lint.\n- **Naming**: Standard Go conventions — PascalCase for exported, camelCase\n  for unexported.\n- **Types**: Prefer explicit types, use type aliases for clarity (e.g.,\n  `type AgentName string`).\n- **Error handling**: Return errors explicitly, use `fmt.Errorf` for\n  wrapping.\n- **Context**: Always pass `context.Context` as first parameter for\n  operations.\n- **Interfaces**: Define interfaces in consuming packages, keep them small\n  and focused.\n- **Structs**: Use struct embedding for composition, group related fields.\n- **Constants**: Use typed constants with iota for enums, group in const\n  blocks.\n- **Testing**: Use testify's `require` package, parallel tests with\n  `t.Parallel()`, `t.SetEnv()` to set environment variables. Always use\n  `t.Tempdir()` when in need of a temporary directory. This directory does\n  not need to be removed.\n- **JSON tags**: Use snake_case for JSON field names.\n- **File permissions**: Use octal notation (0o755, 0o644) for file\n  permissions.\n- **Log messages**: Log messages must start with a capital letter (e.g.,\n  \"Failed to save session\" not \"failed to save session\").\n  - This is enforced by `task lint:log` which runs as part of `task lint`.\n- **Comments**: End comments in periods unless comments are at the end of the\n  line.\n\n## Testing with Mock Providers\n\nWhen writing tests that involve provider configurations, use the mock\nproviders to avoid API calls:\n\n```go\nfunc TestYourFunction(t *testing.T) {\n    // Enable mock providers for testing\n    originalUseMock := config.UseMockProviders\n    config.UseMockProviders = true\n    defer func() {\n        config.UseMockProviders = originalUseMock\n        config.ResetProviders()\n    }()\n\n    // Reset providers to ensure fresh mock data\n    config.ResetProviders()\n\n    // Your test code here - providers will now return mock data\n    providers := config.Providers()\n    // ... test logic\n}\n```\n\n## Formatting\n\n- ALWAYS format any Go code you write.\n  - First, try `gofumpt -w .`.\n  - If `gofumpt` is not available, use `goimports`.\n  - If `goimports` is not available, use `gofmt`.\n  - You can also use `task fmt` to run `gofumpt -w .` on the entire project,\n    as long as `gofumpt` is on the `PATH`.\n\n## Comments\n\n- Comments that live on their own lines should start with capital letters and\n  end with periods. Wrap comments at 78 columns.\n\n## Committing\n\n- ALWAYS use semantic commits (`fix:`, `feat:`, `chore:`, `refactor:`,\n  `docs:`, `sec:`, etc).\n- Try to keep commits to one line, not including your attribution. Only use\n  multi-line commits when additional context is truly necessary.\n\n## Working on the TUI (UI)\n\nAnytime you need to work on the TUI, read `internal/ui/AGENTS.md` before\nstarting work.\n\n## Styling System\n\nThe styling system lives in `internal/ui/styles/` and is organized into\nthree layers:\n\n- **`quickstyle.go`**: The stable base theme builder. `quickStyle(opts)`\n  constructs a `Styles` struct from `quickStyleOpts` — a palette of\n  design tokens (primary, secondary, fgBase, bgBase, success, error, etc.).\n  `quickStyle` must be fully token-driven: never hardcode specific\n  `charmtone.*` colors here (except Chroma syntax highlighting, which is\n  pending tokenization). This lets any theme reuse the base without\n  inheriting Charmtone-specific colors.\n- **`themes.go`**: Defines concrete themes. Each theme function (e.g.\n  `CharmtonePantera`) calls `quickStyle` with its palette, then applies\n  theme-specific overrides as needed.\n- **`styles.go`**: Defines the `Styles` struct and its documentation —\n  the shape of what `quickStyle` produces.\n\n**Adding theme-specific overrides**: When a style genuinely needs a\ncolor that doesn't fit the token model (e.g. the bang prompt uses\nSalt/Hazy/Larple), keep `quickStyle` on the closest semantic token and\noverride only the differing colors in the theme function:\n\n```go\nfunc CharmtonePantera() Styles {\n\ts := quickStyle(quickStyleOpts{ /* palette */ })\n\n\t// Override only the colors that differ from the token defaults.\n\ts.Editor.PromptBangIconFocused = s.Editor.PromptBangIconFocused.\n\t\tForeground(charmtone.Salt).\n\t\tBackground(charmtone.Hazy)\n\n\treturn s\n}\n```\n\n**Adding a new theme**: Add a function in `themes.go` that returns the\nresult of `quickStyle` with a `quickStyleOpts` palette (plus any needed\noverrides), then wire it into `ThemeForProvider`.\n","internal/skills/builtin/crush-config/SKILL.md":"---\nname: crush-config\ndescription: 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.\n---\n\n# Crush Configuration\n\nCrush supports two config formats:\n\n- **`crushrc`** — a Bash script that builds config by calling Crush builtins.\n  **Preferred.** Because it is real Bash you get includes, secrets,\n  conditionals, and variables for free.\n- **`crush.json`** — static JSON. Fully supported; see\n  [Legacy JSON format](#legacy-json-format).\n\nBoth are discovered together and deep-merged. Priority (highest to lowest):\n\n1. `.crushrc` / `crushrc` / `.crush.json` / `crush.json` (project-local,\n   closer-to-cwd wins; Windows uses `.\\.crushrc` / `.\\crushrc`)\n2. `$XDG_CONFIG_HOME/crush/crushrc` or `~/.config/crush/crushrc`\n   (`%XDG_CONFIG_HOME%\\crush\\crushrc` or\n   `%USERPROFILE%\\.config\\crush\\crushrc` on Windows)\n\nData directories (`~/.local/share/crush` and `%LOCALAPPDATA%\\crush`) contain\nmachine-owned JSON state only; Crush does not discover or execute a `crushrc`\nfrom those locations.\n\nIf a directory has both `crushrc` and `crush.json`, they merge (`crushrc` wins\non conflicts) and Crush logs a warning.\n\n## crushrc at a glance\n\nA `crushrc` is a plain Bash script executed at load time with the same embedded\nshell the `bash` tool uses. It builds config by calling builtins (`provider`,\n`model`, `mcp`, `lsp`, `hook`, `permissions`, `option`). Statements run top to\nbottom; later statements win, and `remove`/`reset` operate on anything defined\nearlier or pulled in via `source`.\n\n```bash\n#!/usr/bin/env bash\n# Includes and secrets are just Bash.\nsource ~/.config/crush/shared.sh\n\nprovider add anthropic --api-key \"$ANTHROPIC_API_KEY\"\n\nmodel large anthropic/claude-sonnet-4-20250514 --max-tokens 16384\nmodel small anthropic/claude-haiku-4-20250514\n\noption skill-path ./skills\npermissions allow view ls grep edit\n```\n\nValues are ordinary Bash — quote and expand normally (`\"$VAR\"`, `$(cmd)`,\n`${VAR:?required}`). A failing `$(command)` aborts the load.\n\n`CRUSH_VERSION` is exported into the script so you can feature-detect the\nrunning Crush (it is the literal `devel` for local builds):\n\n```bash\n[[ \"$CRUSH_VERSION\" != devel ]] && lsp add gopls --command gopls\n```\n\n## Commands\n\nAll entity commands are verb-first. `remove` accepts `rm` as an alias. Booleans\naccept `true/false/1/0/yes/no`, case-insensitive.\n\n### providers\n\n```bash\nprovider add <id> [flags]    # define/update; repeated calls merge\nprovider remove <id>         # alias: rm — removes the provider and its models\n```\n\nFlags: `--name`, `--type` (`openai`, `openai-compat`, `anthropic`, or a local\ntype like `ollama`, `lmstudio`, `llamacpp`), `--api-key`, `--base-url`,\n`--disable BOOL`, `--flat-rate BOOL`, `--discover-models BOOL`,\n`--system-prompt-prefix TEXT`, `--extra-header KEY VALUE` (repeatable),\n`--extra-body JSON`, `--provider-options JSON`.\n\n```bash\nprovider add deepseek \\\n  --type openai-compat \\\n  --base-url \"https://api.deepseek.com/v1\" \\\n  --api-key \"${DEEPSEEK_API_KEY:?set DEEPSEEK_API_KEY}\"\n```\n\n### models\n\n```bash\nmodel add <provider>/<id> [flags]      # register a custom model (provider must exist)\nmodel remove <provider>/<id>           # alias: rm\nmodel large [<provider>/<id>] [flags]  # set the large slot; no arg prints it\nmodel small [<provider>/<id>] [flags]  # set the small slot; no arg prints it\n```\n\n- `<provider>/<id>` is the same form `crush models` prints. A missing slash is\n  an error. `model add` requires the provider to already exist.\n- `model add` flags: `--name`, `--context-window N`, `--default-max-tokens N`,\n  `--can-reason BOOL`, `--supports-images BOOL`, `--price-input F`,\n  `--price-output F`, `--price-cache-create F`, `--price-cache-hit F`,\n  `--reasoning-effort low|medium|high`.\n- `model large`/`model small` flags: `--think`, `--reasoning-effort`,\n  `--max-tokens N`, `--temperature F`, `--top-p F`, `--top-k N`,\n  `--frequency-penalty F`, `--presence-penalty F`, `--provider-options JSON`.\n- `model large` with no argument prints the current selection as `provider/id`,\n  usable in `$(model large)`.\n\n`large` is the primary coding model; `small` is used for summarization.\n\n### mcp\n\n```bash\nmcp add <name> --type stdio|sse|http [flags]   # default type is stdio\nmcp remove <name>                              # alias: rm\n```\n\nFlags: `--command CMD`, `--args ARG` (repeatable), `--env KEY VALUE`\n(repeatable), `--url URL`, `--header KEY VALUE` (repeatable), `--timeout N`,\n`--disabled BOOL`, `--disabled-tools TOOL` (repeatable), `--enabled-tools TOOL`\n(repeatable), `--oauth BOOL`, `--oauth-client-id ID`, `--oauth-client-secret SECRET`,\n`--oauth-callback-port PORT`.\n\n```bash\nmcp add github --type http \\\n  --url \"https://api.githubcopilot.com/mcp/\" \\\n  --header Authorization \"Bearer $GH_PAT\"\n\nmcp add filesystem --command node --args /path/to/mcp-server.js\n```\n\n### lsp\n\n```bash\nlsp add <name> --command CMD [flags]\nlsp remove <name>                     # alias: rm\n```\n\nFlags: `--args ARG` (repeatable), `--env KEY VALUE` (repeatable),\n`--filetypes TYPE` (repeatable), `--root-markers MARKER` (repeatable),\n`--timeout N`, `--disabled BOOL`, `--init-options JSON`, `--options JSON`.\n\n```bash\nlsp add go --command gopls --env GOPATH \"$HOME/go\"\nlsp add typescript --command typescript-language-server --args --stdio\n```\n\n### hooks\n\n```bash\nhook add <event> --command CMD [--name NAME] [--matcher REGEX] [--timeout N]\nhook remove <event> [--name NAME]    # alias: rm; without --name clears the event\n```\n\nOnly named hooks can be removed individually — give a hook `--name` if you\nintend to remove it later. See [Hooks runtime](#hooks-runtime) for how hooks\nexecute (stdin payload, env vars, decisions).\n\n```bash\nhook add PreToolUse --matcher \"^bash$\" --command \".crush/hooks/no-haskell.sh\" --name no-haskell\n```\n\n### permissions\n\n```bash\npermissions allow <tool> [<tool> ...]   # tools that skip permission prompts\npermissions deny <tool> [<tool> ...]    # hide tools from the agent entirely\n```\n\n`deny` is the inverse of `allow`: it writes `options.disabled_tools`. A denied\ntool is hidden from the agent, not merely prompted for.\n\n### options\n\n```bash\noption <key> [value]\noption reset <list-key>    # clear a list option back to empty\n```\n\n- **Boolean keys** (value optional, defaults `true`): `debug`, `debug-lsp`,\n  `auto-lsp`, `progress`.\n- **Boolean keys phrased positively** (stored as the negated field): `metrics`,\n  `auto-summarize`, `provider-auto-update`,\n  `default-providers`. Example: `option metrics false` disables metrics.\n- **String keys**: `data-directory`, `initialize-as`, `notifications`.\n- **Attribution keys**: `attribution-trailer-style` (`none`, `co-authored-by`,\n  `assisted-by`) and `attribution-generated-with` (boolean).\n- **UI settings**: `option ui compact BOOL`, `option ui diff unified|split`,\n  `option ui transparent BOOL`, `option ui mouse BOOL` (default `true`;\n  disable to let the terminal/tmux handle selection, copy/paste, and\n  scrolling), `option ui scrollbar default|always|never`,\n  `option ui exit-banner default|compact|none`,\n  `option ui completions-max-depth N`, `option ui completions-max-items N`.\n- **List keys** (singular, one value per call, repeatable): `context-path`,\n  `global-context-path`, `skill-path`, `disable-skill`. Use `option reset <key>`\n  to wipe inherited values (e.g. after `source`).\n\n```bash\noption progress false\noption skill-path ./skills\noption disable-skill crush-config\noption attribution-trailer-style assisted-by\noption attribution-generated-with true\noption ui compact true\noption ui diff unified\noption ui exit-banner compact\n```\n\n> [!IMPORTANT] These skill paths are loaded by default and do NOT need\n> `skill-path`: `.agents/skills`, `.crush/skills`, `.claude/skills`,\n> `.cursor/skills`.\n\n## Hooks runtime\n\nHooks are user-defined shell commands that fire on agent events. Currently only\n`PreToolUse` is supported, which runs before a tool executes. This behavior is\nthe same however the hook is defined (`hook add` or JSON).\n\n### How hooks work\n\n1. When a tool is about to be called, all `PreToolUse` hooks with a matching\n   `matcher` (or no matcher) run in parallel.\n2. Duplicate commands are deduplicated — each unique command runs at most once.\n3. The hook receives JSON on **stdin** and hook-specific **environment\n   variables**.\n\nEvent names are case-insensitive and accept snake_case: `PreToolUse`,\n`pretooluse`, `pre_tool_use`, `PRE_TOOL_USE` all work.\n\n### Hook input (stdin)\n\n```json\n{\n  \"event\": \"PreToolUse\",\n  \"session_id\": \"abc-123\",\n  \"cwd\": \"/path/to/project\",\n  \"tool_name\": \"bash\",\n  \"tool_input\": { \"command\": \"ls -la\" }\n}\n```\n\n### Hook environment variables\n\n| Variable                     | Description                                       |\n| ---------------------------- | ------------------------------------------------- |\n| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)                    |\n| `CRUSH_TOOL_NAME`            | Name of the tool being called                     |\n| `CRUSH_SESSION_ID`           | Current session ID                                |\n| `CRUSH_CWD`                  | Current working directory                         |\n| `CRUSH_PROJECT_DIR`          | Project root directory                            |\n| `CRUSH_TOOL_INPUT_COMMAND`   | Value of `command` from tool input (if present)   |\n| `CRUSH_TOOL_INPUT_FILE_PATH` | Value of `file_path` from tool input (if present) |\n\n### Hook output\n\n**Exit code 0** — hook succeeded. Stdout is parsed as JSON:\n\n```json\n{ \"decision\": \"allow\", \"context\": \"optional context appended to tool result\" }\n```\n\n- `decision`: `allow` to explicitly allow, `deny` to block, `none` (or omit).\n- `reason`: explanation (used when denying).\n- `context`: extra context appended to the tool result.\n- `updated_input`: replacement JSON for the tool input; last non-empty wins.\n\n**Exit code 2** — the tool call is blocked; stderr is the deny reason.\n\n**Any other exit code** — non-blocking error; the tool call proceeds.\n\n### Decision aggregation\n\n- **Deny wins over allow** — any deny blocks the call.\n- **Allow wins over none** — a lone allow lets it proceed.\n- Deny reasons and context strings are concatenated (newline-separated).\n- For `updated_input`, the last non-empty value wins.\n\n### Claude Code compatibility\n\nCrush also accepts the Claude Code hook output format, so existing hooks work\nunchanged:\n\n```json\n{\n  \"hookSpecificOutput\": {\n    \"permissionDecision\": \"allow\",\n    \"permissionDecisionReason\": \"Auto-approved\",\n    \"updatedInput\": { \"command\": \"echo rewritten\" }\n  }\n}\n```\n\n## User-invocable skills\n\nSkills can be invoked as commands. Add `user-invocable: true` to the skill's\nYAML frontmatter:\n\n```yaml\n---\nname: my-skill\ndescription: A skill that can be invoked as a command.\nuser-invocable: true\n---\n```\n\n- Global skills appear as `user:skill-name`; project skills as\n  `project:skill-name`.\n- Add `disable-model-invocation: true` to keep a skill user-only (hidden from\n  the model's available-skills list but still manually invocable).\n\n## Environment variables\n\n- `CRUSH_VERSION` — exported into `crushrc` at load; the running version (or\n  `devel` for local builds).\n- `CRUSH_GLOBAL_CONFIG` — override global config location.\n- `CRUSH_GLOBAL_DATA` — override data directory location.\n- `CRUSH_SKILLS_DIR` — override default skills directory.\n\n## Legacy JSON format\n\n`crush.json` is the original static format. It still works and merges with\n`crushrc`. Basic structure:\n\n```json\n{\n  \"$schema\": \"https://charm.land/crush.json\",\n  \"models\": {},\n  \"providers\": {},\n  \"mcp\": {},\n  \"lsp\": {},\n  \"hooks\": {},\n  \"options\": {},\n  \"permissions\": {}\n}\n```\n\nThe `$schema` property enables IDE autocomplete but is optional.\n\n### crushrc ↔ crush.json mapping\n\n| crushrc                             | crush.json                                             |\n| ------------------------------------ | ------------------------------------------------------ |\n| `provider add openai --api-key \"$K\"` | `providers.openai = {\"api_key\": \"$K\"}`                 |\n| `model add openai/gpt-x --name X`    | append to `providers.openai.models[]`                  |\n| `model large openai/gpt-x`           | `models.large = {\"provider\":\"openai\",\"model\":\"gpt-x\"}` |\n| `mcp add gh --type http --url U`     | `mcp.gh = {\"type\":\"http\",\"url\":\"U\"}`                   |\n| `lsp add go --command gopls`         | `lsp.go = {\"command\":\"gopls\"}`                         |\n| `hook add PreToolUse --command C`    | append to `hooks.PreToolUse[]`                         |\n| `permissions allow view ls`          | `permissions.allowed_tools = [\"view\",\"ls\"]`            |\n| `permissions deny bash`              | `options.disabled_tools = [\"bash\"]`                    |\n| `option skill-path ./skills`         | `options.skills_paths = [\"./skills\"]`                  |\n| `option metrics false`               | `options.disable_metrics = true`                       |\n| `option request-timeout 300`          | `options.request_timeout = 300`                        |\n| `option max-background-jobs 200`     | `options.max_background_jobs = 200`                    |\n| `option attribution-trailer-style none` | `options.attribution.trailer_style = \"none\"`        |\n| `option attribution-generated-with false` | `options.attribution.generated_with = false`       |\n\n### Shell expansion in crush.json\n\nIn JSON, only selected string fields are run through the embedded shell at load\ntime (in `crushrc`, everything is native Bash so this table does not apply):\n\n| Surface                                                         | Expansion                          |\n| --------------------------------------------------------------- | ---------------------------------- |\n| Provider `api_key`, `base_url`, `api_endpoint`, `extra_headers` | yes                                |\n| Provider `extra_body`                                           | **no** (JSON passthrough)          |\n| MCP `command`, `args`, `env`, `headers`, `url`                  | yes                                |\n| LSP `command`, `args`, `env`                                    | yes                                |\n| Hook `command`                                                  | runs via `sh -c`, not the resolver |\n\nSupported constructs: `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:+alt}`,\n`${VAR:?message}`, `$(command)`. An unset variable expands to empty; a failing\n`$(command)` is a hard error. A header that resolves to empty is dropped from\nthe request.\n\n### Security note\n\nBoth formats are trusted code. `crushrc` runs entirely, and any `$(...)` in\n`crush.json` runs at load time, with the invoking user's shell privileges,\nbefore the UI appears. Don't launch Crush in a directory whose config you\nhaven't reviewed.\n","internal/skills/builtin/crush-hooks/SKILL.md":"---\nname: crush-hooks\ndescription: Use when the user wants to add, write, debug, or configure a Crush hook — gating or blocking tool calls, approving or rewriting tool input before execution, injecting context into tool results, or troubleshooting hook behavior in crush.json.\n---\n\n# Crush Hooks\n\nHooks are user-defined commands in `crush.json` that fire at specific points\nduring execution, giving deterministic control over tool behavior. They run\n**before** permission checks and **only on the top-level agent's** tool calls —\nsub-agent calls (task tool, agentic_fetch, etc.) are not intercepted, though\nthe sub-agent tool call itself is.\n\nFor the full reference, see `docs/hooks/README.md`. This skill covers what you\nneed to author correct hooks.\n\n## Supported Events\n\nOnly `PreToolUse` is currently supported. Event names are case-insensitive and\naccept snake_case (`PreToolUse`, `pretooluse`, `pre_tool_use` all work).\n\n## Configuration\n\n```jsonc\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"^bash$\",              // regex against tool name (optional; omit to match all)\n        \"command\": \"./hooks/my-hook.sh\",   // required: shell command to run\n        \"timeout\": 10                     // optional: seconds, default 30\n      }\n    ]\n  }\n}\n```\n\nProject-level hooks take precedence over global. Matching hooks are deduped by\n`command`, run in parallel, and aggregated in **config order** (not finish order).\n\n## Language\n\n`command` is a shell command, so hooks can be written in any language by\ninvoking the interpreter: `node ./hooks/h.js`, `python3 ./hooks/h.py`,\n`./hooks/h.sh`, inline `echo '…'`, etc. The rest of this skill shows bash, but\nthe input/output contract is identical regardless of language.\n\n## Input\n\n**Environment variables:**\n\n| Variable                     | Description                              |\n| ---------------------------- | ---------------------------------------- |\n| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)           |\n| `CRUSH_TOOL_NAME`            | Tool being called (e.g. `bash`)          |\n| `CRUSH_SESSION_ID`           | Current session ID                       |\n| `CRUSH_CWD`                  | Working directory                        |\n| `CRUSH_PROJECT_DIR`          | Project root directory                   |\n| `CRUSH_TOOL_INPUT_COMMAND`   | For `bash` calls: the shell command      |\n| `CRUSH_TOOL_INPUT_FILE_PATH` | For file tools: the target file path     |\n\n**JSON on stdin:**\n\n```json\n{\n  \"event\": \"PreToolUse\",\n  \"session_id\": \"313909e\",\n  \"cwd\": \"/home/user/project\",\n  \"tool_name\": \"bash\",\n  \"tool_input\": {\"command\": \"rm -rf /\"}\n}\n```\n\n## Output\n\nCommunicate back via exit code (+ stderr) or JSON on stdout.\n\n| Exit Code | Meaning                                                       |\n| --------- | ------------------------------------------------------------- |\n| 0         | Success. Stdout is parsed as the JSON envelope below.         |\n| 2         | Block this tool call. Stderr becomes the deny reason.         |\n| 49        | Halt the whole turn. Stderr becomes the halt reason.          |\n| Other     | Non-blocking error. Logged and ignored; tool call proceeds.   |\n\nExit 2 blocks one tool call (agent sees the reason and can try again); exit 49\nends the whole turn (user takes over). Default to deny — reach for halt only\nwhen letting the agent retry is itself the problem (e.g. secrets detected,\npolicy violation).\n\n**JSON envelope (exit 0):**\n\n```json\n{\n  \"version\": 1,\n  \"decision\": \"allow\",\n  \"halt\": false,\n  \"reason\": \"...\",\n  \"context\": \"Extra info for the model\",\n  \"updated_input\": {\"command\": \"rewritten\"}\n}\n```\n\n- **`decision`**: `\"allow\"`, `\"deny\"`, or omit. `\"allow\"` is **affirmative\n  pre-approval** — it bypasses the permission prompt entirely. Omit it\n  (or `null`) when you only want to inject context or rewrite input without\n  also auto-approving the call.\n- **`halt: true`**: ends the turn (same as exit 49).\n- **`reason`**: shown to the model on deny; to model and user on halt.\n- **`context`**: string **or array of strings**. Appended to what the model\n  sees. Empty entries are dropped.\n- **`updated_input`**: **shallow-merge patch** against `tool_input`, not a\n  replacement. Keys you include overwrite; keys you don't are preserved.\n  Nested objects are replaced wholesale, not deep-merged. Ignored on deny/halt.\n\n## Aggregation (Multiple Hooks)\n\nComposed in **config order**:\n\n- `deny` > `allow` > no opinion. First deny decides; subsequent allows don't override.\n- `halt` is sticky: any hook halting ends the turn.\n- `reason` and `context` concatenate in config order (newline-joined).\n- `updated_input` patches shallow-merge sequentially; later patches win on colliding keys.\n\n## Canonical Examples\n\n### Block destructive commands\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nif echo \"$CRUSH_TOOL_INPUT_COMMAND\" | grep -qE 'rm\\s+-(rf|fr)\\s+/'; then\n  echo \"Refusing to run rm -rf against root\" >&2\n  exit 2\nfi\n```\n\nConfig: `{\"matcher\": \"^bash$\", \"command\": \"./hooks/no-rm-rf.sh\"}`\n\n### Auto-approve read-only tools (inline, no script)\n\n```jsonc\n{\"matcher\": \"^(view|ls|grep|glob)$\", \"command\": \"echo '{\\\"decision\\\":\\\"allow\\\"}'\"}\n```\n\nEvery `view`/`ls`/`grep`/`glob` call now runs without prompting.\n\n### Inject context without auto-approving\n\nEmit only `context` — omit `decision` so the normal permission flow still runs.\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nif [[ \"$CRUSH_TOOL_INPUT_FILE_PATH\" == *.go ]]; then\n  echo '{\"context\": \"Remember: run gofumpt after editing Go files.\"}'\nelse\n  echo '{}'\nfi\n```\n\nConfig: `{\"matcher\": \"^(edit|write|multiedit)$\", \"command\": \"./hooks/go-context.sh\"}`\n\n### Rewrite tool input (shallow merge)\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nread -r input\nrewritten=$(echo \"$input\" | jq -r '.tool_input.command' | some-rewriter)\n\ncat <<EOF\n{\n  \"context\": \"Rewrote command\",\n  \"updated_input\": {\"command\": \"$rewritten\"}\n}\nEOF\n```\n\nIf the original call was `{\"command\": \"npm test\", \"timeout\": 60000}`, the\ntool runs with `{\"command\": \"<rewritten>\", \"timeout\": 60000}` — `timeout` is\npreserved.\n\n## Authoring Checklist\n\n1. Add `#!/usr/bin/env bash` and `set -euo pipefail` (for shell scripts).\n2. `chmod +x` the script.\n3. Add the entry under `hooks.PreToolUse` in `crush.json` with the right matcher.\n4. Decide intent: inject context (omit `decision`), auto-approve (`\"allow\"`),\n   block (`exit 2`), or halt (`exit 49`).\n5. If rewriting input, remember `updated_input` is a shallow merge — only\n   include the keys you want to change.\n\n## Debugging\n\n- Timeouts kill the hook silently and the tool call proceeds. Bump `timeout` if needed.\n- Non-zero exit codes other than 2/49 are logged but don't block — check Crush logs.\n- Use `echo \"debug info\" >&2` for logging without corrupting stdout JSON.\n- `matcher` is a regex against the tool name. Use `^bash$` (not `bash`) if you\n  don't also want to match `mcp_something_bash`.\n\n## Claude Code Compatibility\n\nCrush also accepts Claude Code's `hookSpecificOutput` envelope. One intentional\ndivergence: Crush treats `updated_input` as shallow-merge, Claude Code replaces.\nExisting Claude Code hooks work without modification for the matcher/decision\nparts; revisit any that relied on `updatedInput` fully replacing tool input.\n","internal/skills/builtin/jq/SKILL.md":"---\nname: jq\ndescription: Use when the user needs to query, filter, reshape, extract, create, or construct JSON data — including API responses, config files, log output, or any structured data — or when helping the user write or debug JSON transformations.\n---\n\n# jq — Built-in JSON Processor\n\nCrush ships a built-in `jq` command (via `github.com/itchyny/gojq`) available\nin the bash tool. No external binary is required.\n\n## Supported Flags\n\n| Flag | Description |\n|------|-------------|\n| `-r`, `--raw-output` | Output strings without quotes |\n| `-j`, `--join-output` | Like `-r` but no trailing newline |\n| `-c`, `--compact-output` | One-line JSON output |\n| `-s`, `--slurp` | Read all inputs into an array |\n| `-n`, `--null-input` | Use `null` as input (ignore stdin) |\n| `-e`, `--exit-status` | Exit 1 if last output is `false` or `null` |\n| `-R`, `--raw-input` | Read each line as a string, not JSON |\n| `--arg name value` | Bind `$name` to a string value |\n| `--argjson name value` | Bind `$name` to a parsed JSON value |\n\nFile arguments after the filter are also supported: `jq '.foo' file.json`.\n\n## Differences from Standard jq\n\nThe built-in uses gojq, which is a pure-Go jq implementation. Key\ndifferences:\n\n- **No object key ordering** — keys are sorted by default; `keys_unsorted`\n  and `-S` are unavailable.\n- **Arbitrary-precision integers** — large integers keep full precision\n  (addition, subtraction, multiplication, modulo, division when divisible).\n- **String indexing** — `\"abcde\"[2]` returns `\"c\"`.\n- **Not supported** — `--ascii-output`, `--seq`, `--stream`,\n  `--stream-errors`, `-f`/`--from-file`, `--slurpfile`, `--rawfile`,\n  `--args`, `--jsonargs`, `input_line_number`, `$__loc__`, some regex\n  features (backreferences, look-around).\n- **YAML** — gojq supports `--yaml-input`/`--yaml-output` but the\n  built-in does not currently expose these flags.\n\n## Common Patterns\n\nExtract a field:\n```sh\necho '{\"name\":\"crush\"}' | jq '.name'\n```\n\nFilter an array:\n```sh\necho '[1,2,3,4,5]' | jq '[.[] | select(. > 3)]'\n```\n\nReshape objects:\n```sh\necho '{\"first\":\"Ada\",\"last\":\"Lovelace\"}' | jq '{full: (.first + \" \" + .last)}'\n```\n\nUse variables:\n```sh\necho '{}' | jq --arg host localhost --argjson port 8080 '{host: $host, port: $port}'\n```\n\nSlurp multiple JSON values:\n```sh\necho '{\"a\":1}{\"b\":2}' | jq -s '.'\n```\n\nCompact output for piping:\n```sh\necho '{\"a\":1}' | jq -c '.a += 1'\n```\n\nRaw string output:\n```sh\necho '[\"one\",\"two\",\"three\"]' | jq -r '.[]'\n```\n\nProcess a file:\n```sh\njq '.dependencies | keys' package.json\n```\n\nNull input for constructing JSON:\n```sh\njq -n --arg msg hello '{\"message\": $msg}'\n```\n\n## Tips\n\n- Pipe jq output to other commands: `jq -r '.url' data.json | xargs curl`\n- Chain filters with `|` inside the expression, not shell pipes.\n- Use `try` to suppress errors on missing keys: `jq 'try .foo.bar'`\n- Use `// \"default\"` for fallback values: `jq '.name // \"unknown\"'`\n- Use `@csv`, `@tsv`, `@base64`, `@html`, `@uri` for format strings.\n"},"files":{".agents/skills/builtin-skills/SKILL.md":"---\nname: builtin-skills\ndescription:\n  Use when creating a new builtin skill for Crush, editing an existing builtin\n  skill (internal/skills/builtin/), or when the user needs to understand how the\n  embedded skill system works.\n---\n\n# Builtin Skills\n\nCrush embeds skills directly into the binary via `internal/skills/builtin/`.\nThese are always available without user configuration.\n\n## How It Works\n\n- Each skill lives in `internal/skills/builtin/<skill-name>/SKILL.md`.\n- The tree is embedded at compile time via `//go:embed builtin/*` in\n  `internal/skills/embed.go`.\n- `DiscoverBuiltin()` walks the embedded FS, parses each `SKILL.md`, and sets\n  paths with the `crush://skills/` prefix (e.g., `crush://skills/jq/SKILL.md`).\n- The View tool resolves `crush://` paths from the embedded FS, not disk.\n- User skills with the same name override builtins (last occurrence wins in\n  `Deduplicate()`).\n\n## Adding a New Builtin Skill\n\n1. Create `internal/skills/builtin/<skill-name>/SKILL.md` with YAML frontmatter\n   (`name`, `description`) and markdown instructions. The directory name must\n   match the `name` field.\n2. No extra wiring needed — `//go:embed builtin/*` picks up new directories\n   automatically.\n3. Add a test assertion in `TestDiscoverBuiltin` in\n   `internal/skills/skills_test.go` to verify discovery.\n4. Build and test: `go build . && go test ./internal/skills/...`\n\n## Existing Builtin Skills\n\n| Skill          | Directory               | Description                                |\n| -------------- | ----------------------- | ------------------------------------------ |\n| `crush-config` | `builtin/crush-config/` | Crush configuration help                   |\n| `crush-hooks`  | `builtin/crush-hooks/`  | Authoring, configuring and debugging hooks |\n| `jq`           | `builtin/jq/`           | jq JSON processor usage guide              |\n",".agents/skills/shell-builtins/SKILL.md":"---\nname: shell-builtins\ndescription: Use when creating a new shell builtin command for Crush (internal/shell/), editing an existing one, or when the user needs to understand how commands are intercepted in Crush's embedded shell.\n---\n\n# Shell Builtins\n\nCrush's shell (`internal/shell/`) uses `mvdan.cc/sh/v3` for POSIX shell\nemulation. Commands can be intercepted before they reach the OS by adding\n**builtins** — functions handled in-process.\n\n## How Builtins Work\n\nBuiltins live in `builtinHandler()` in `internal/shell/run.go`. This is an\n`interp.ExecHandlerFunc` middleware registered in `standardHandlers()`\n**before** the block handler, so builtins run even for commands that would\notherwise be blocked. The same handler chain is shared by the stateful\n`Shell` type and the stateless `Run` entrypoint used by the hook runner,\nso builtins are available identically in the `bash` tool and in hooks.\n\nThe handler is a switch on `args[0]`. Each case either handles the command\ninline or delegates to a helper function.\n\n## Adding a New Builtin\n\n1. **Add the case** to the switch in `builtinHandler()` in `run.go`.\n2. **Get I/O from the handler context**, not from `os.Stdin`/`os.Stdout`.\n   This ensures the builtin works with pipes and redirections:\n   ```go\n   case \"mycommand\":\n       hc := interp.HandlerCtx(ctx)\n       return handleMyCommand(ctx, args, hc.Stdin, hc.Stdout, hc.Stderr)\n   ```\n3. **Implement the handler** in its own file (e.g.,\n   `internal/shell/mycommand.go`). The function signature must accept a\n   `context.Context` as the first parameter, plus args, stdin, stdout, and\n   stderr:\n   ```go\n   func handleMyCommand(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {\n       // args[0] is the command name (\"mycommand\"), args[1:] are arguments.\n       // Write output to stdout, errors to stderr.\n       // Return nil on success, or interp.ExitStatus(n) for non-zero exit codes.\n   }\n   ```\n4. **Poll `ctx` in every unbounded loop.** Builtins that iterate over\n   input, emit values in a generator-style loop, or do any other work\n   that can exceed a few milliseconds MUST check `ctx.Err()` on each\n   iteration and return it verbatim when non-nil. Hook timeouts rely on\n   this: an unbounded builtin that never polls ctx cannot be interrupted\n   by a hook's `timeout_sec`, and the hook runner will have to abandon\n   the goroutine (see `internal/hooks/runner.go`). Returning `ctx.Err()`\n   (not `interp.ExitStatus(n)`) lets callers distinguish \"command exited\n   non-zero\" from \"we ran out of time\".\n   ```go\n   for _, item := range items {\n       if err := ctx.Err(); err != nil {\n           return err\n       }\n       // ... process item\n   }\n   ```\n5. **Return values**: return `nil` for success, `interp.ExitStatus(n)` for\n   non-zero exit codes, or `ctx.Err()` on cancellation. Write error\n   messages to `stderr` before returning.\n6. **No extra wiring needed** — `builtinHandler()` is already registered\n   in `standardHandlers()`.\n\n## Existing Builtins\n\n| Command | File | Description |\n|---------|------|-------------|\n| `jq` | `jq.go` | JSON processor using `github.com/itchyny/gojq` |\n","AGENTS.md":"# Crush Development Guide\n\n## Project Overview\n\nCrush is a terminal-based AI coding assistant built in Go by\n[Charm](https://charm.land). It connects to LLMs and gives them tools to read,\nwrite, and execute code. It supports multiple providers (Anthropic, OpenAI,\nGemini, Bedrock, Copilot, Hyper, MiniMax, Vercel, and more), integrates with\nLSPs for code intelligence, and supports extensibility via MCP servers and\nagent skills.\n\nThe module path is `github.com/charmbracelet/crush`.\n\n## Architecture\n\n```\nmain.go                            CLI entry point (cobra via internal/cmd)\ninternal/\n  app/app.go                       Top-level wiring: DB, config, agents, LSP, MCP, events\n  cmd/                             CLI commands (root, run, login, models, stats, sessions)\n  config/\n    config.go                      Config struct, context file paths, agent definitions\n    load.go                        crushrc and crush.json loading and validation\n    provider.go                    Provider configuration and model resolution\n  shellconfig/                      Bash-powered config format (crushrc builtins)\n  agent/\n    agent.go                       SessionAgent: runs LLM conversations per session\n    coordinator.go                 Coordinator: manages named agents (\"coder\", \"task\")\n    hooked_tool.go                 Decorator that runs PreToolUse hooks before tool execution\n    prompts.go                     Loads Go-template system prompts\n    templates/                     System prompt templates (coder.md.tpl, task.md.tpl, etc.)\n    tools/                         All built-in tools (bash, edit, view, grep, glob, etc.)\n      mcp/                         MCP client integration\n  hooks/                           Hook engine: runs user shell commands on hook events\n    hooks.go                       Decision types, aggregation logic, event constants\n    runner.go                      Parallel hook execution, timeout, dedup\n    input.go                       Stdin payload builder, env vars, stdout parsing (Crush + Claude Code compat)\n  session/session.go               Session CRUD backed by SQLite\n  message/                         Message model and content types\n  db/                              SQLite via sqlc, with migrations\n    sql/                           Raw SQL queries (consumed by sqlc)\n    migrations/                    Schema migrations\n  lsp/                             LSP client manager, auto-discovery, on-demand startup\n  ui/                              Bubble Tea v2 TUI (see internal/ui/AGENTS.md)\n  permission/                      Tool permission checking and allow-lists\n  skills/                          Skill file discovery and loading\n  shell/                           Bash command execution with background job support\n  event/                           Telemetry (PostHog)\n  pubsub/                          Internal pub/sub for cross-component messaging\n  filetracker/                     Tracks files touched per session\n  history/                         Prompt history\n```\n\n### Key Dependency Roles\n\n- **`charm.land/fantasy`**: LLM provider abstraction layer. Handles protocol\n  differences between Anthropic, OpenAI, Gemini, etc. Used in `internal/app`\n  and `internal/agent`.\n- **`charm.land/bubbletea/v2`**: TUI framework powering the interactive UI.\n- **`charm.land/lipgloss/v2`**: Terminal styling.\n- **`charm.land/glamour/v2`**: Markdown rendering in the terminal.\n- **`charm.land/catwalk`**: Snapshot/golden-file testing for TUI components.\n- **`sqlc`**: Generates Go code from SQL queries in `internal/db/sql/`.\n\n### Key Patterns\n\n- **Config is a Service**: accessed via `config.Service`, not global state.\n- **Tools are self-documenting**: each tool has a `.go` implementation and a\n  `.md` description file in `internal/agent/tools/`.\n- **System prompts are Go templates**: `internal/agent/templates/*.md.tpl`\n  with runtime data injected.\n- **Context files**: Crush reads AGENTS.md, CRUSH.md, CLAUDE.md, GEMINI.md\n  (and `.local` variants) from the working directory for project-specific\n  instructions.\n- **Bash config format**: Crush's primary config format is `crushrc` — a\n  Bash script using builtins (`provider`, `model`, `mcp`, `lsp`,\n  `permissions`, `hook`, `options`) to define config. `crush.json` is still\n  supported but is deprecated in favor of `crushrc` and may be removed in a\n  future release. Shell config files are discovered alongside JSON configs\n  and deep-merged through the same pipeline. Builtins are registered via\n  `shell.RegisterBuiltin` and gated by a `ConfigBuilder` on the context —\n  they are no-ops during normal bash tool execution. See\n  `internal/shellconfig/`.\n- **Persistence**: SQLite + sqlc. All queries live in `internal/db/sql/`,\n  generated code in `internal/db/`. Migrations in `internal/db/migrations/`.\n- **Pub/sub**: `internal/pubsub` for decoupled communication between agent,\n  UI, and services.\n- **Hooks**: User-defined shell commands in `crushrc` (or `crush.json`)\n  that fire before tool execution. The engine (`internal/hooks/`) is\n  independent of fantasy and agent — it takes inputs, runs commands,\n  returns decisions. The `hookedTool` decorator in\n  `internal/agent/hooked_tool.go` wraps tools at the coordinator level.\n  Hooks run before permission checks. See `HOOKS.md` for the user-facing\n  protocol.\n- **CGO disabled**: builds with `CGO_ENABLED=0` and\n  `GOEXPERIMENT=greenteagc`.\n\n## Build/Test/Lint Commands\n\n- **Build**: `go build .` or `go run .`\n- **Test**: `task test` or `go test ./...` (run single test:\n  `go test ./internal/llm/prompt -run TestGetContextFromPaths`)\n- **Update Golden Files**: `go test ./... -update` (regenerates `.golden`\n  files when test output changes)\n  - Update specific package:\n    `go test ./internal/tui/components/core -update` (in this case,\n    we're updating \"core\")\n- **Lint**: `task lint:fix`\n- **Format**: `task fmt` (`gofumpt -w .`)\n- **Modernize**: `task modernize` (runs `modernize` which makes code\n  simplifications)\n- **Dev**: `task dev` (runs with profiling enabled)\n\n## Code Style Guidelines\n\n- **Imports**: Use `goimports` formatting, group stdlib, external, internal\n  packages.\n- **Formatting**: Use gofumpt (stricter than gofmt), enabled in\n  golangci-lint.\n- **Naming**: Standard Go conventions — PascalCase for exported, camelCase\n  for unexported.\n- **Types**: Prefer explicit types, use type aliases for clarity (e.g.,\n  `type AgentName string`).\n- **Error handling**: Return errors explicitly, use `fmt.Errorf` for\n  wrapping.\n- **Context**: Always pass `context.Context` as first parameter for\n  operations.\n- **Interfaces**: Define interfaces in consuming packages, keep them small\n  and focused.\n- **Structs**: Use struct embedding for composition, group related fields.\n- **Constants**: Use typed constants with iota for enums, group in const\n  blocks.\n- **Testing**: Use testify's `require` package, parallel tests with\n  `t.Parallel()`, `t.SetEnv()` to set environment variables. Always use\n  `t.Tempdir()` when in need of a temporary directory. This directory does\n  not need to be removed.\n- **JSON tags**: Use snake_case for JSON field names.\n- **File permissions**: Use octal notation (0o755, 0o644) for file\n  permissions.\n- **Log messages**: Log messages must start with a capital letter (e.g.,\n  \"Failed to save session\" not \"failed to save session\").\n  - This is enforced by `task lint:log` which runs as part of `task lint`.\n- **Comments**: End comments in periods unless comments are at the end of the\n  line.\n\n## Testing with Mock Providers\n\nWhen writing tests that involve provider configurations, use the mock\nproviders to avoid API calls:\n\n```go\nfunc TestYourFunction(t *testing.T) {\n    // Enable mock providers for testing\n    originalUseMock := config.UseMockProviders\n    config.UseMockProviders = true\n    defer func() {\n        config.UseMockProviders = originalUseMock\n        config.ResetProviders()\n    }()\n\n    // Reset providers to ensure fresh mock data\n    config.ResetProviders()\n\n    // Your test code here - providers will now return mock data\n    providers := config.Providers()\n    // ... test logic\n}\n```\n\n## Formatting\n\n- ALWAYS format any Go code you write.\n  - First, try `gofumpt -w .`.\n  - If `gofumpt` is not available, use `goimports`.\n  - If `goimports` is not available, use `gofmt`.\n  - You can also use `task fmt` to run `gofumpt -w .` on the entire project,\n    as long as `gofumpt` is on the `PATH`.\n\n## Comments\n\n- Comments that live on their own lines should start with capital letters and\n  end with periods. Wrap comments at 78 columns.\n\n## Committing\n\n- ALWAYS use semantic commits (`fix:`, `feat:`, `chore:`, `refactor:`,\n  `docs:`, `sec:`, etc).\n- Try to keep commits to one line, not including your attribution. Only use\n  multi-line commits when additional context is truly necessary.\n\n## Working on the TUI (UI)\n\nAnytime you need to work on the TUI, read `internal/ui/AGENTS.md` before\nstarting work.\n\n## Styling System\n\nThe styling system lives in `internal/ui/styles/` and is organized into\nthree layers:\n\n- **`quickstyle.go`**: The stable base theme builder. `quickStyle(opts)`\n  constructs a `Styles` struct from `quickStyleOpts` — a palette of\n  design tokens (primary, secondary, fgBase, bgBase, success, error, etc.).\n  `quickStyle` must be fully token-driven: never hardcode specific\n  `charmtone.*` colors here (except Chroma syntax highlighting, which is\n  pending tokenization). This lets any theme reuse the base without\n  inheriting Charmtone-specific colors.\n- **`themes.go`**: Defines concrete themes. Each theme function (e.g.\n  `CharmtonePantera`) calls `quickStyle` with its palette, then applies\n  theme-specific overrides as needed.\n- **`styles.go`**: Defines the `Styles` struct and its documentation —\n  the shape of what `quickStyle` produces.\n\n**Adding theme-specific overrides**: When a style genuinely needs a\ncolor that doesn't fit the token model (e.g. the bang prompt uses\nSalt/Hazy/Larple), keep `quickStyle` on the closest semantic token and\noverride only the differing colors in the theme function:\n\n```go\nfunc CharmtonePantera() Styles {\n\ts := quickStyle(quickStyleOpts{ /* palette */ })\n\n\t// Override only the colors that differ from the token defaults.\n\ts.Editor.PromptBangIconFocused = s.Editor.PromptBangIconFocused.\n\t\tForeground(charmtone.Salt).\n\t\tBackground(charmtone.Hazy)\n\n\treturn s\n}\n```\n\n**Adding a new theme**: Add a function in `themes.go` that returns the\nresult of `quickStyle` with a `quickStyleOpts` palette (plus any needed\noverrides), then wire it into `ThemeForProvider`.\n","internal/skills/builtin/crush-config/SKILL.md":"---\nname: crush-config\ndescription: 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.\n---\n\n# Crush Configuration\n\nCrush supports two config formats:\n\n- **`crushrc`** — a Bash script that builds config by calling Crush builtins.\n  **Preferred.** Because it is real Bash you get includes, secrets,\n  conditionals, and variables for free.\n- **`crush.json`** — static JSON. Fully supported; see\n  [Legacy JSON format](#legacy-json-format).\n\nBoth are discovered together and deep-merged. Priority (highest to lowest):\n\n1. `.crushrc` / `crushrc` / `.crush.json` / `crush.json` (project-local,\n   closer-to-cwd wins; Windows uses `.\\.crushrc` / `.\\crushrc`)\n2. `$XDG_CONFIG_HOME/crush/crushrc` or `~/.config/crush/crushrc`\n   (`%XDG_CONFIG_HOME%\\crush\\crushrc` or\n   `%USERPROFILE%\\.config\\crush\\crushrc` on Windows)\n\nData directories (`~/.local/share/crush` and `%LOCALAPPDATA%\\crush`) contain\nmachine-owned JSON state only; Crush does not discover or execute a `crushrc`\nfrom those locations.\n\nIf a directory has both `crushrc` and `crush.json`, they merge (`crushrc` wins\non conflicts) and Crush logs a warning.\n\n## crushrc at a glance\n\nA `crushrc` is a plain Bash script executed at load time with the same embedded\nshell the `bash` tool uses. It builds config by calling builtins (`provider`,\n`model`, `mcp`, `lsp`, `hook`, `permissions`, `option`). Statements run top to\nbottom; later statements win, and `remove`/`reset` operate on anything defined\nearlier or pulled in via `source`.\n\n```bash\n#!/usr/bin/env bash\n# Includes and secrets are just Bash.\nsource ~/.config/crush/shared.sh\n\nprovider add anthropic --api-key \"$ANTHROPIC_API_KEY\"\n\nmodel large anthropic/claude-sonnet-4-20250514 --max-tokens 16384\nmodel small anthropic/claude-haiku-4-20250514\n\noption skill-path ./skills\npermissions allow view ls grep edit\n```\n\nValues are ordinary Bash — quote and expand normally (`\"$VAR\"`, `$(cmd)`,\n`${VAR:?required}`). A failing `$(command)` aborts the load.\n\n`CRUSH_VERSION` is exported into the script so you can feature-detect the\nrunning Crush (it is the literal `devel` for local builds):\n\n```bash\n[[ \"$CRUSH_VERSION\" != devel ]] && lsp add gopls --command gopls\n```\n\n## Commands\n\nAll entity commands are verb-first. `remove` accepts `rm` as an alias. Booleans\naccept `true/false/1/0/yes/no`, case-insensitive.\n\n### providers\n\n```bash\nprovider add <id> [flags]    # define/update; repeated calls merge\nprovider remove <id>         # alias: rm — removes the provider and its models\n```\n\nFlags: `--name`, `--type` (`openai`, `openai-compat`, `anthropic`, or a local\ntype like `ollama`, `lmstudio`, `llamacpp`), `--api-key`, `--base-url`,\n`--disable BOOL`, `--flat-rate BOOL`, `--discover-models BOOL`,\n`--system-prompt-prefix TEXT`, `--extra-header KEY VALUE` (repeatable),\n`--extra-body JSON`, `--provider-options JSON`.\n\n```bash\nprovider add deepseek \\\n  --type openai-compat \\\n  --base-url \"https://api.deepseek.com/v1\" \\\n  --api-key \"${DEEPSEEK_API_KEY:?set DEEPSEEK_API_KEY}\"\n```\n\n### models\n\n```bash\nmodel add <provider>/<id> [flags]      # register a custom model (provider must exist)\nmodel remove <provider>/<id>           # alias: rm\nmodel large [<provider>/<id>] [flags]  # set the large slot; no arg prints it\nmodel small [<provider>/<id>] [flags]  # set the small slot; no arg prints it\n```\n\n- `<provider>/<id>` is the same form `crush models` prints. A missing slash is\n  an error. `model add` requires the provider to already exist.\n- `model add` flags: `--name`, `--context-window N`, `--default-max-tokens N`,\n  `--can-reason BOOL`, `--supports-images BOOL`, `--price-input F`,\n  `--price-output F`, `--price-cache-create F`, `--price-cache-hit F`,\n  `--reasoning-effort low|medium|high`.\n- `model large`/`model small` flags: `--think`, `--reasoning-effort`,\n  `--max-tokens N`, `--temperature F`, `--top-p F`, `--top-k N`,\n  `--frequency-penalty F`, `--presence-penalty F`, `--provider-options JSON`.\n- `model large` with no argument prints the current selection as `provider/id`,\n  usable in `$(model large)`.\n\n`large` is the primary coding model; `small` is used for summarization.\n\n### mcp\n\n```bash\nmcp add <name> --type stdio|sse|http [flags]   # default type is stdio\nmcp remove <name>                              # alias: rm\n```\n\nFlags: `--command CMD`, `--args ARG` (repeatable), `--env KEY VALUE`\n(repeatable), `--url URL`, `--header KEY VALUE` (repeatable), `--timeout N`,\n`--disabled BOOL`, `--disabled-tools TOOL` (repeatable), `--enabled-tools TOOL`\n(repeatable), `--oauth BOOL`, `--oauth-client-id ID`, `--oauth-client-secret SECRET`,\n`--oauth-callback-port PORT`.\n\n```bash\nmcp add github --type http \\\n  --url \"https://api.githubcopilot.com/mcp/\" \\\n  --header Authorization \"Bearer $GH_PAT\"\n\nmcp add filesystem --command node --args /path/to/mcp-server.js\n```\n\n### lsp\n\n```bash\nlsp add <name> --command CMD [flags]\nlsp remove <name>                     # alias: rm\n```\n\nFlags: `--args ARG` (repeatable), `--env KEY VALUE` (repeatable),\n`--filetypes TYPE` (repeatable), `--root-markers MARKER` (repeatable),\n`--timeout N`, `--disabled BOOL`, `--init-options JSON`, `--options JSON`.\n\n```bash\nlsp add go --command gopls --env GOPATH \"$HOME/go\"\nlsp add typescript --command typescript-language-server --args --stdio\n```\n\n### hooks\n\n```bash\nhook add <event> --command CMD [--name NAME] [--matcher REGEX] [--timeout N]\nhook remove <event> [--name NAME]    # alias: rm; without --name clears the event\n```\n\nOnly named hooks can be removed individually — give a hook `--name` if you\nintend to remove it later. See [Hooks runtime](#hooks-runtime) for how hooks\nexecute (stdin payload, env vars, decisions).\n\n```bash\nhook add PreToolUse --matcher \"^bash$\" --command \".crush/hooks/no-haskell.sh\" --name no-haskell\n```\n\n### permissions\n\n```bash\npermissions allow <tool> [<tool> ...]   # tools that skip permission prompts\npermissions deny <tool> [<tool> ...]    # hide tools from the agent entirely\n```\n\n`deny` is the inverse of `allow`: it writes `options.disabled_tools`. A denied\ntool is hidden from the agent, not merely prompted for.\n\n### options\n\n```bash\noption <key> [value]\noption reset <list-key>    # clear a list option back to empty\n```\n\n- **Boolean keys** (value optional, defaults `true`): `debug`, `debug-lsp`,\n  `auto-lsp`, `progress`.\n- **Boolean keys phrased positively** (stored as the negated field): `metrics`,\n  `auto-summarize`, `provider-auto-update`,\n  `default-providers`. Example: `option metrics false` disables metrics.\n- **String keys**: `data-directory`, `initialize-as`, `notifications`.\n- **Attribution keys**: `attribution-trailer-style` (`none`, `co-authored-by`,\n  `assisted-by`) and `attribution-generated-with` (boolean).\n- **UI settings**: `option ui compact BOOL`, `option ui diff unified|split`,\n  `option ui transparent BOOL`, `option ui mouse BOOL` (default `true`;\n  disable to let the terminal/tmux handle selection, copy/paste, and\n  scrolling), `option ui scrollbar default|always|never`,\n  `option ui exit-banner default|compact|none`,\n  `option ui completions-max-depth N`, `option ui completions-max-items N`.\n- **List keys** (singular, one value per call, repeatable): `context-path`,\n  `global-context-path`, `skill-path`, `disable-skill`. Use `option reset <key>`\n  to wipe inherited values (e.g. after `source`).\n\n```bash\noption progress false\noption skill-path ./skills\noption disable-skill crush-config\noption attribution-trailer-style assisted-by\noption attribution-generated-with true\noption ui compact true\noption ui diff unified\noption ui exit-banner compact\n```\n\n> [!IMPORTANT] These skill paths are loaded by default and do NOT need\n> `skill-path`: `.agents/skills`, `.crush/skills`, `.claude/skills`,\n> `.cursor/skills`.\n\n## Hooks runtime\n\nHooks are user-defined shell commands that fire on agent events. Currently only\n`PreToolUse` is supported, which runs before a tool executes. This behavior is\nthe same however the hook is defined (`hook add` or JSON).\n\n### How hooks work\n\n1. When a tool is about to be called, all `PreToolUse` hooks with a matching\n   `matcher` (or no matcher) run in parallel.\n2. Duplicate commands are deduplicated — each unique command runs at most once.\n3. The hook receives JSON on **stdin** and hook-specific **environment\n   variables**.\n\nEvent names are case-insensitive and accept snake_case: `PreToolUse`,\n`pretooluse`, `pre_tool_use`, `PRE_TOOL_USE` all work.\n\n### Hook input (stdin)\n\n```json\n{\n  \"event\": \"PreToolUse\",\n  \"session_id\": \"abc-123\",\n  \"cwd\": \"/path/to/project\",\n  \"tool_name\": \"bash\",\n  \"tool_input\": { \"command\": \"ls -la\" }\n}\n```\n\n### Hook environment variables\n\n| Variable                     | Description                                       |\n| ---------------------------- | ------------------------------------------------- |\n| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)                    |\n| `CRUSH_TOOL_NAME`            | Name of the tool being called                     |\n| `CRUSH_SESSION_ID`           | Current session ID                                |\n| `CRUSH_CWD`                  | Current working directory                         |\n| `CRUSH_PROJECT_DIR`          | Project root directory                            |\n| `CRUSH_TOOL_INPUT_COMMAND`   | Value of `command` from tool input (if present)   |\n| `CRUSH_TOOL_INPUT_FILE_PATH` | Value of `file_path` from tool input (if present) |\n\n### Hook output\n\n**Exit code 0** — hook succeeded. Stdout is parsed as JSON:\n\n```json\n{ \"decision\": \"allow\", \"context\": \"optional context appended to tool result\" }\n```\n\n- `decision`: `allow` to explicitly allow, `deny` to block, `none` (or omit).\n- `reason`: explanation (used when denying).\n- `context`: extra context appended to the tool result.\n- `updated_input`: replacement JSON for the tool input; last non-empty wins.\n\n**Exit code 2** — the tool call is blocked; stderr is the deny reason.\n\n**Any other exit code** — non-blocking error; the tool call proceeds.\n\n### Decision aggregation\n\n- **Deny wins over allow** — any deny blocks the call.\n- **Allow wins over none** — a lone allow lets it proceed.\n- Deny reasons and context strings are concatenated (newline-separated).\n- For `updated_input`, the last non-empty value wins.\n\n### Claude Code compatibility\n\nCrush also accepts the Claude Code hook output format, so existing hooks work\nunchanged:\n\n```json\n{\n  \"hookSpecificOutput\": {\n    \"permissionDecision\": \"allow\",\n    \"permissionDecisionReason\": \"Auto-approved\",\n    \"updatedInput\": { \"command\": \"echo rewritten\" }\n  }\n}\n```\n\n## User-invocable skills\n\nSkills can be invoked as commands. Add `user-invocable: true` to the skill's\nYAML frontmatter:\n\n```yaml\n---\nname: my-skill\ndescription: A skill that can be invoked as a command.\nuser-invocable: true\n---\n```\n\n- Global skills appear as `user:skill-name`; project skills as\n  `project:skill-name`.\n- Add `disable-model-invocation: true` to keep a skill user-only (hidden from\n  the model's available-skills list but still manually invocable).\n\n## Environment variables\n\n- `CRUSH_VERSION` — exported into `crushrc` at load; the running version (or\n  `devel` for local builds).\n- `CRUSH_GLOBAL_CONFIG` — override global config location.\n- `CRUSH_GLOBAL_DATA` — override data directory location.\n- `CRUSH_SKILLS_DIR` — override default skills directory.\n\n## Legacy JSON format\n\n`crush.json` is the original static format. It still works and merges with\n`crushrc`. Basic structure:\n\n```json\n{\n  \"$schema\": \"https://charm.land/crush.json\",\n  \"models\": {},\n  \"providers\": {},\n  \"mcp\": {},\n  \"lsp\": {},\n  \"hooks\": {},\n  \"options\": {},\n  \"permissions\": {}\n}\n```\n\nThe `$schema` property enables IDE autocomplete but is optional.\n\n### crushrc ↔ crush.json mapping\n\n| crushrc                             | crush.json                                             |\n| ------------------------------------ | ------------------------------------------------------ |\n| `provider add openai --api-key \"$K\"` | `providers.openai = {\"api_key\": \"$K\"}`                 |\n| `model add openai/gpt-x --name X`    | append to `providers.openai.models[]`                  |\n| `model large openai/gpt-x`           | `models.large = {\"provider\":\"openai\",\"model\":\"gpt-x\"}` |\n| `mcp add gh --type http --url U`     | `mcp.gh = {\"type\":\"http\",\"url\":\"U\"}`                   |\n| `lsp add go --command gopls`         | `lsp.go = {\"command\":\"gopls\"}`                         |\n| `hook add PreToolUse --command C`    | append to `hooks.PreToolUse[]`                         |\n| `permissions allow view ls`          | `permissions.allowed_tools = [\"view\",\"ls\"]`            |\n| `permissions deny bash`              | `options.disabled_tools = [\"bash\"]`                    |\n| `option skill-path ./skills`         | `options.skills_paths = [\"./skills\"]`                  |\n| `option metrics false`               | `options.disable_metrics = true`                       |\n| `option request-timeout 300`          | `options.request_timeout = 300`                        |\n| `option max-background-jobs 200`     | `options.max_background_jobs = 200`                    |\n| `option attribution-trailer-style none` | `options.attribution.trailer_style = \"none\"`        |\n| `option attribution-generated-with false` | `options.attribution.generated_with = false`       |\n\n### Shell expansion in crush.json\n\nIn JSON, only selected string fields are run through the embedded shell at load\ntime (in `crushrc`, everything is native Bash so this table does not apply):\n\n| Surface                                                         | Expansion                          |\n| --------------------------------------------------------------- | ---------------------------------- |\n| Provider `api_key`, `base_url`, `api_endpoint`, `extra_headers` | yes                                |\n| Provider `extra_body`                                           | **no** (JSON passthrough)          |\n| MCP `command`, `args`, `env`, `headers`, `url`                  | yes                                |\n| LSP `command`, `args`, `env`                                    | yes                                |\n| Hook `command`                                                  | runs via `sh -c`, not the resolver |\n\nSupported constructs: `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:+alt}`,\n`${VAR:?message}`, `$(command)`. An unset variable expands to empty; a failing\n`$(command)` is a hard error. A header that resolves to empty is dropped from\nthe request.\n\n### Security note\n\nBoth formats are trusted code. `crushrc` runs entirely, and any `$(...)` in\n`crush.json` runs at load time, with the invoking user's shell privileges,\nbefore the UI appears. Don't launch Crush in a directory whose config you\nhaven't reviewed.\n","internal/skills/builtin/crush-hooks/SKILL.md":"---\nname: crush-hooks\ndescription: Use when the user wants to add, write, debug, or configure a Crush hook — gating or blocking tool calls, approving or rewriting tool input before execution, injecting context into tool results, or troubleshooting hook behavior in crush.json.\n---\n\n# Crush Hooks\n\nHooks are user-defined commands in `crush.json` that fire at specific points\nduring execution, giving deterministic control over tool behavior. They run\n**before** permission checks and **only on the top-level agent's** tool calls —\nsub-agent calls (task tool, agentic_fetch, etc.) are not intercepted, though\nthe sub-agent tool call itself is.\n\nFor the full reference, see `docs/hooks/README.md`. This skill covers what you\nneed to author correct hooks.\n\n## Supported Events\n\nOnly `PreToolUse` is currently supported. Event names are case-insensitive and\naccept snake_case (`PreToolUse`, `pretooluse`, `pre_tool_use` all work).\n\n## Configuration\n\n```jsonc\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"^bash$\",              // regex against tool name (optional; omit to match all)\n        \"command\": \"./hooks/my-hook.sh\",   // required: shell command to run\n        \"timeout\": 10                     // optional: seconds, default 30\n      }\n    ]\n  }\n}\n```\n\nProject-level hooks take precedence over global. Matching hooks are deduped by\n`command`, run in parallel, and aggregated in **config order** (not finish order).\n\n## Language\n\n`command` is a shell command, so hooks can be written in any language by\ninvoking the interpreter: `node ./hooks/h.js`, `python3 ./hooks/h.py`,\n`./hooks/h.sh`, inline `echo '…'`, etc. The rest of this skill shows bash, but\nthe input/output contract is identical regardless of language.\n\n## Input\n\n**Environment variables:**\n\n| Variable                     | Description                              |\n| ---------------------------- | ---------------------------------------- |\n| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)           |\n| `CRUSH_TOOL_NAME`            | Tool being called (e.g. `bash`)          |\n| `CRUSH_SESSION_ID`           | Current session ID                       |\n| `CRUSH_CWD`                  | Working directory                        |\n| `CRUSH_PROJECT_DIR`          | Project root directory                   |\n| `CRUSH_TOOL_INPUT_COMMAND`   | For `bash` calls: the shell command      |\n| `CRUSH_TOOL_INPUT_FILE_PATH` | For file tools: the target file path     |\n\n**JSON on stdin:**\n\n```json\n{\n  \"event\": \"PreToolUse\",\n  \"session_id\": \"313909e\",\n  \"cwd\": \"/home/user/project\",\n  \"tool_name\": \"bash\",\n  \"tool_input\": {\"command\": \"rm -rf /\"}\n}\n```\n\n## Output\n\nCommunicate back via exit code (+ stderr) or JSON on stdout.\n\n| Exit Code | Meaning                                                       |\n| --------- | ------------------------------------------------------------- |\n| 0         | Success. Stdout is parsed as the JSON envelope below.         |\n| 2         | Block this tool call. Stderr becomes the deny reason.         |\n| 49        | Halt the whole turn. Stderr becomes the halt reason.          |\n| Other     | Non-blocking error. Logged and ignored; tool call proceeds.   |\n\nExit 2 blocks one tool call (agent sees the reason and can try again); exit 49\nends the whole turn (user takes over). Default to deny — reach for halt only\nwhen letting the agent retry is itself the problem (e.g. secrets detected,\npolicy violation).\n\n**JSON envelope (exit 0):**\n\n```json\n{\n  \"version\": 1,\n  \"decision\": \"allow\",\n  \"halt\": false,\n  \"reason\": \"...\",\n  \"context\": \"Extra info for the model\",\n  \"updated_input\": {\"command\": \"rewritten\"}\n}\n```\n\n- **`decision`**: `\"allow\"`, `\"deny\"`, or omit. `\"allow\"` is **affirmative\n  pre-approval** — it bypasses the permission prompt entirely. Omit it\n  (or `null`) when you only want to inject context or rewrite input without\n  also auto-approving the call.\n- **`halt: true`**: ends the turn (same as exit 49).\n- **`reason`**: shown to the model on deny; to model and user on halt.\n- **`context`**: string **or array of strings**. Appended to what the model\n  sees. Empty entries are dropped.\n- **`updated_input`**: **shallow-merge patch** against `tool_input`, not a\n  replacement. Keys you include overwrite; keys you don't are preserved.\n  Nested objects are replaced wholesale, not deep-merged. Ignored on deny/halt.\n\n## Aggregation (Multiple Hooks)\n\nComposed in **config order**:\n\n- `deny` > `allow` > no opinion. First deny decides; subsequent allows don't override.\n- `halt` is sticky: any hook halting ends the turn.\n- `reason` and `context` concatenate in config order (newline-joined).\n- `updated_input` patches shallow-merge sequentially; later patches win on colliding keys.\n\n## Canonical Examples\n\n### Block destructive commands\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nif echo \"$CRUSH_TOOL_INPUT_COMMAND\" | grep -qE 'rm\\s+-(rf|fr)\\s+/'; then\n  echo \"Refusing to run rm -rf against root\" >&2\n  exit 2\nfi\n```\n\nConfig: `{\"matcher\": \"^bash$\", \"command\": \"./hooks/no-rm-rf.sh\"}`\n\n### Auto-approve read-only tools (inline, no script)\n\n```jsonc\n{\"matcher\": \"^(view|ls|grep|glob)$\", \"command\": \"echo '{\\\"decision\\\":\\\"allow\\\"}'\"}\n```\n\nEvery `view`/`ls`/`grep`/`glob` call now runs without prompting.\n\n### Inject context without auto-approving\n\nEmit only `context` — omit `decision` so the normal permission flow still runs.\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nif [[ \"$CRUSH_TOOL_INPUT_FILE_PATH\" == *.go ]]; then\n  echo '{\"context\": \"Remember: run gofumpt after editing Go files.\"}'\nelse\n  echo '{}'\nfi\n```\n\nConfig: `{\"matcher\": \"^(edit|write|multiedit)$\", \"command\": \"./hooks/go-context.sh\"}`\n\n### Rewrite tool input (shallow merge)\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nread -r input\nrewritten=$(echo \"$input\" | jq -r '.tool_input.command' | some-rewriter)\n\ncat <<EOF\n{\n  \"context\": \"Rewrote command\",\n  \"updated_input\": {\"command\": \"$rewritten\"}\n}\nEOF\n```\n\nIf the original call was `{\"command\": \"npm test\", \"timeout\": 60000}`, the\ntool runs with `{\"command\": \"<rewritten>\", \"timeout\": 60000}` — `timeout` is\npreserved.\n\n## Authoring Checklist\n\n1. Add `#!/usr/bin/env bash` and `set -euo pipefail` (for shell scripts).\n2. `chmod +x` the script.\n3. Add the entry under `hooks.PreToolUse` in `crush.json` with the right matcher.\n4. Decide intent: inject context (omit `decision`), auto-approve (`\"allow\"`),\n   block (`exit 2`), or halt (`exit 49`).\n5. If rewriting input, remember `updated_input` is a shallow merge — only\n   include the keys you want to change.\n\n## Debugging\n\n- Timeouts kill the hook silently and the tool call proceeds. Bump `timeout` if needed.\n- Non-zero exit codes other than 2/49 are logged but don't block — check Crush logs.\n- Use `echo \"debug info\" >&2` for logging without corrupting stdout JSON.\n- `matcher` is a regex against the tool name. Use `^bash$` (not `bash`) if you\n  don't also want to match `mcp_something_bash`.\n\n## Claude Code Compatibility\n\nCrush also accepts Claude Code's `hookSpecificOutput` envelope. One intentional\ndivergence: Crush treats `updated_input` as shallow-merge, Claude Code replaces.\nExisting Claude Code hooks work without modification for the matcher/decision\nparts; revisit any that relied on `updatedInput` fully replacing tool input.\n","internal/skills/builtin/jq/SKILL.md":"---\nname: jq\ndescription: Use when the user needs to query, filter, reshape, extract, create, or construct JSON data — including API responses, config files, log output, or any structured data — or when helping the user write or debug JSON transformations.\n---\n\n# jq — Built-in JSON Processor\n\nCrush ships a built-in `jq` command (via `github.com/itchyny/gojq`) available\nin the bash tool. No external binary is required.\n\n## Supported Flags\n\n| Flag | Description |\n|------|-------------|\n| `-r`, `--raw-output` | Output strings without quotes |\n| `-j`, `--join-output` | Like `-r` but no trailing newline |\n| `-c`, `--compact-output` | One-line JSON output |\n| `-s`, `--slurp` | Read all inputs into an array |\n| `-n`, `--null-input` | Use `null` as input (ignore stdin) |\n| `-e`, `--exit-status` | Exit 1 if last output is `false` or `null` |\n| `-R`, `--raw-input` | Read each line as a string, not JSON |\n| `--arg name value` | Bind `$name` to a string value |\n| `--argjson name value` | Bind `$name` to a parsed JSON value |\n\nFile arguments after the filter are also supported: `jq '.foo' file.json`.\n\n## Differences from Standard jq\n\nThe built-in uses gojq, which is a pure-Go jq implementation. Key\ndifferences:\n\n- **No object key ordering** — keys are sorted by default; `keys_unsorted`\n  and `-S` are unavailable.\n- **Arbitrary-precision integers** — large integers keep full precision\n  (addition, subtraction, multiplication, modulo, division when divisible).\n- **String indexing** — `\"abcde\"[2]` returns `\"c\"`.\n- **Not supported** — `--ascii-output`, `--seq`, `--stream`,\n  `--stream-errors`, `-f`/`--from-file`, `--slurpfile`, `--rawfile`,\n  `--args`, `--jsonargs`, `input_line_number`, `$__loc__`, some regex\n  features (backreferences, look-around).\n- **YAML** — gojq supports `--yaml-input`/`--yaml-output` but the\n  built-in does not currently expose these flags.\n\n## Common Patterns\n\nExtract a field:\n```sh\necho '{\"name\":\"crush\"}' | jq '.name'\n```\n\nFilter an array:\n```sh\necho '[1,2,3,4,5]' | jq '[.[] | select(. > 3)]'\n```\n\nReshape objects:\n```sh\necho '{\"first\":\"Ada\",\"last\":\"Lovelace\"}' | jq '{full: (.first + \" \" + .last)}'\n```\n\nUse variables:\n```sh\necho '{}' | jq --arg host localhost --argjson port 8080 '{host: $host, port: $port}'\n```\n\nSlurp multiple JSON values:\n```sh\necho '{\"a\":1}{\"b\":2}' | jq -s '.'\n```\n\nCompact output for piping:\n```sh\necho '{\"a\":1}' | jq -c '.a += 1'\n```\n\nRaw string output:\n```sh\necho '[\"one\",\"two\",\"three\"]' | jq -r '.[]'\n```\n\nProcess a file:\n```sh\njq '.dependencies | keys' package.json\n```\n\nNull input for constructing JSON:\n```sh\njq -n --arg msg hello '{\"message\": $msg}'\n```\n\n## Tips\n\n- Pipe jq output to other commands: `jq -r '.url' data.json | xargs curl`\n- Chain filters with `|` inside the expression, not shell pipes.\n- Use `try` to suppress errors on missing keys: `jq 'try .foo.bar'`\n- Use `// \"default\"` for fallback values: `jq '.name // \"unknown\"'`\n- Use `@csv`, `@tsv`, `@base64`, `@html`, `@uri` for format strings.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/charmbracelet/crush/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Crush Development Guide\n\n## Project Overview\n\nCrush is a terminal-based AI coding assistant built in Go by\n[Charm](https://charm.land). It connects to LLMs and gives them tools to read,\nwrite, and execute code. It supports multiple providers (Anthropic, OpenAI,\nGemini, Bedrock, Copilot, Hyper, MiniMax, Vercel, and more), integrates with\nLSPs for code intelligence, and supports extensibility via MCP servers and\nagent skills.\n\nThe module path is `github.com/charmbracelet/crush`.\n\n## Architecture\n\n```\nmain.go                            CLI entry point (cobra via internal/cmd)\ninternal/\n  app/app.go                       Top-level wiring: DB, config, agents, LSP, MCP, events\n  cmd/                             CLI commands (root, run, login, models, stats, sessions)\n  config/\n    config.go                      Config struct, context file paths, agent definitions\n    load.go                        crushrc and crush.json loading and validation\n    provider.go                    Provider configuration and model resolution\n  shellconfig/                      Bash-powered config format (crushrc builtins)\n  agent/\n    agent.go                       SessionAgent: runs LLM conversations per session\n    coordinator.go                 Coordinator: manages named agents (\"coder\", \"task\")\n    hooked_tool.go                 Decorator that runs PreToolUse hooks before tool execution\n    prompts.go                     Loads Go-template system prompts\n    templates/                     System prompt templates (coder.md.tpl, task.md.tpl, etc.)\n    tools/                         All built-in tools (bash, edit, view, grep, glob, etc.)\n      mcp/                         MCP client integration\n  hooks/                           Hook engine: runs user shell commands on hook events\n    hooks.go                       Decision types, aggregation logic, event constants\n    runner.go                      Parallel hook execution, timeout, dedup\n    input.go                       Stdin payload builder, env vars, stdout parsing (Crush + Claude Code compat)\n  session/session.go               Session CRUD backed by SQLite\n  message/                         Message model and content types\n  db/                              SQLite via sqlc, with migrations\n    sql/                           Raw SQL queries (consumed by sqlc)\n    migrations/                    Schema migrations\n  lsp/                             LSP client manager, auto-discovery, on-demand startup\n  ui/                              Bubble Tea v2 TUI (see internal/ui/AGENTS.md)\n  permission/                      Tool permission checking and allow-lists\n  skills/                          Skill file discovery and loading\n  shell/                           Bash command execution with background job support\n  event/                           Telemetry (PostHog)\n  pubsub/                          Internal pub/sub for cross-component messaging\n  filetracker/                     Tracks files touched per session\n  history/                         Prompt history\n```\n\n### Key Dependency Roles\n\n- **`charm.land/fantasy`**: LLM provider abstraction layer. Handles protocol\n  differences between Anthropic, OpenAI, Gemini, etc. Used in `internal/app`\n  and `internal/agent`.\n- **`charm.land/bubbletea/v2`**: TUI framework powering the interactive UI.\n- **`charm.land/lipgloss/v2`**: Terminal styling.\n- **`charm.land/glamour/v2`**: Markdown rendering in the terminal.\n- **`charm.land/catwalk`**: Snapshot/golden-file testing for TUI components.\n- **`sqlc`**: Generates Go code from SQL queries in `internal/db/sql/`.\n\n### Key Patterns\n\n- **Config is a Service**: accessed via `config.Service`, not global state.\n- **Tools are self-documenting**: each tool has a `.go` implementation and a\n  `.md` description file in `internal/agent/tools/`.\n- **System prompts are Go templates**: `internal/agent/templates/*.md.tpl`\n  with runtime data injected.\n- **Context files**: Crush reads AGENTS.md, CRUSH.md, CLAUDE.md, GEMINI.md\n  (and `.local` variants) from the working directory for project-specific\n  instructions.\n- **Bash config format**: Crush's primary config format is `crushrc` — a\n  Bash script using builtins (`provider`, `model`, `mcp`, `lsp`,\n  `permissions`, `hook`, `options`) to define config. `crush.json` is still\n  supported but is deprecated in favor of `crushrc` and may be removed in a\n  future release. Shell config files are discovered alongside JSON configs\n  and deep-merged through the same pipeline. Builtins are registered via\n  `shell.RegisterBuiltin` and gated by a `ConfigBuilder` on the context —\n  they are no-ops during normal bash tool execution. See\n  `internal/shellconfig/`.\n- **Persistence**: SQLite + sqlc. All queries live in `internal/db/sql/`,\n  generated code in `internal/db/`. Migrations in `internal/db/migrations/`.\n- **Pub/sub**: `internal/pubsub` for decoupled communication between agent,\n  UI, and services.\n- **Hooks**: User-defined shell commands in `crushrc` (or `crush.json`)\n  that fire before tool execution. The engine (`internal/hooks/`) is\n  independent of fantasy and agent — it takes inputs, runs commands,\n  returns decisions. The `hookedTool` decorator in\n  `internal/agent/hooked_tool.go` wraps tools at the coordinator level.\n  Hooks run before permission checks. See `HOOKS.md` for the user-facing\n  protocol.\n- **CGO disabled**: builds with `CGO_ENABLED=0` and\n  `GOEXPERIMENT=greenteagc`.\n\n## Build/Test/Lint Commands\n\n- **Build**: `go build .` or `go run .`\n- **Test**: `task test` or `go test ./...` (run single test:\n  `go test ./internal/llm/prompt -run TestGetContextFromPaths`)\n- **Update Golden Files**: `go test ./... -update` (regenerates `.golden`\n  files when test output changes)\n  - Update specific package:\n    `go test ./internal/tui/components/core -update` (in this case,\n    we're updating \"core\")\n- **Lint**: `task lint:fix`\n- **Format**: `task fmt` (`gofumpt -w .`)\n- **Modernize**: `task modernize` (runs `modernize` which makes code\n  simplifications)\n- **Dev**: `task dev` (runs with profiling enabled)\n\n## Code Style Guidelines\n\n- **Imports**: Use `goimports` formatting, group stdlib, external, internal\n  packages.\n- **Formatting**: Use gofumpt (stricter than gofmt), enabled in\n  golangci-lint.\n- **Naming**: Standard Go conventions — PascalCase for exported, camelCase\n  for unexported.\n- **Types**: Prefer explicit types, use type aliases for clarity (e.g.,\n  `type AgentName string`).\n- **Error handling**: Return errors explicitly, use `fmt.Errorf` for\n  wrapping.\n- **Context**: Always pass `context.Context` as first parameter for\n  operations.\n- **Interfaces**: Define interfaces in consuming packages, keep them small\n  and focused.\n- **Structs**: Use struct embedding for composition, group related fields.\n- **Constants**: Use typed constants with iota for enums, group in const\n  blocks.\n- **Testing**: Use testify's `require` package, parallel tests with\n  `t.Parallel()`, `t.SetEnv()` to set environment variables. Always use\n  `t.Tempdir()` when in need of a temporary directory. This directory does\n  not need to be removed.\n- **JSON tags**: Use snake_case for JSON field names.\n- **File permissions**: Use octal notation (0o755, 0o644) for file\n  permissions.\n- **Log messages**: Log messages must start with a capital letter (e.g.,\n  \"Failed to save session\" not \"failed to save session\").\n  - This is enforced by `task lint:log` which runs as part of `task lint`.\n- **Comments**: End comments in periods unless comments are at the end of the\n  line.\n\n## Testing with Mock Providers\n\nWhen writing tests that involve provider configurations, use the mock\nproviders to avoid API calls:\n\n```go\nfunc TestYourFunction(t *testing.T) {\n    // Enable mock providers for testing\n    originalUseMock := config.UseMockProviders\n    config.UseMockProviders = true\n    defer func() {\n        config.UseMockProviders = originalUseMock\n        config.ResetProviders()\n    }()\n\n    // Reset providers to ensure fresh mock data\n    config.ResetProviders()\n\n    // Your test code here - providers will now return mock data\n    providers := config.Providers()\n    // ... test logic\n}\n```\n\n## Formatting\n\n- ALWAYS format any Go code you write.\n  - First, try `gofumpt -w .`.\n  - If `gofumpt` is not available, use `goimports`.\n  - If `goimports` is not available, use `gofmt`.\n  - You can also use `task fmt` to run `gofumpt -w .` on the entire project,\n    as long as `gofumpt` is on the `PATH`.\n\n## Comments\n\n- Comments that live on their own lines should start with capital letters and\n  end with periods. Wrap comments at 78 columns.\n\n## Committing\n\n- ALWAYS use semantic commits (`fix:`, `feat:`, `chore:`, `refactor:`,\n  `docs:`, `sec:`, etc).\n- Try to keep commits to one line, not including your attribution. Only use\n  multi-line commits when additional context is truly necessary.\n\n## Working on the TUI (UI)\n\nAnytime you need to work on the TUI, read `internal/ui/AGENTS.md` before\nstarting work.\n\n## Styling System\n\nThe styling system lives in `internal/ui/styles/` and is organized into\nthree layers:\n\n- **`quickstyle.go`**: The stable base theme builder. `quickStyle(opts)`\n  constructs a `Styles` struct from `quickStyleOpts` — a palette of\n  design tokens (primary, secondary, fgBase, bgBase, success, error, etc.).\n  `quickStyle` must be fully token-driven: never hardcode specific\n  `charmtone.*` colors here (except Chroma syntax highlighting, which is\n  pending tokenization). This lets any theme reuse the base without\n  inheriting Charmtone-specific colors.\n- **`themes.go`**: Defines concrete themes. Each theme function (e.g.\n  `CharmtonePantera`) calls `quickStyle` with its palette, then applies\n  theme-specific overrides as needed.\n- **`styles.go`**: Defines the `Styles` struct and its documentation —\n  the shape of what `quickStyle` produces.\n\n**Adding theme-specific overrides**: When a style genuinely needs a\ncolor that doesn't fit the token model (e.g. the bang prompt uses\nSalt/Hazy/Larple), keep `quickStyle` on the closest semantic token and\noverride only the differing colors in the theme function:\n\n```go\nfunc CharmtonePantera() Styles {\n\ts := quickStyle(quickStyleOpts{ /* palette */ })\n\n\t// Override only the colors that differ from the token defaults.\n\ts.Editor.PromptBangIconFocused = s.Editor.PromptBangIconFocused.\n\t\tForeground(charmtone.Salt).\n\t\tBackground(charmtone.Hazy)\n\n\treturn s\n}\n```\n\n**Adding a new theme**: Add a function in `themes.go` that returns the\nresult of `quickStyle` with a `quickStyleOpts` palette (plus any needed\noverrides), then wire it into `ThemeForProvider`.\n","isInternal":false,"tokens":2486,"sizeBytes":10603},{"name":"SKILL.md","path":".agents/skills/builtin-skills/SKILL.md","rawUrl":"https://raw.githubusercontent.com/charmbracelet/crush/HEAD/.agents/skills/builtin-skills/SKILL.md","title":"builtin-skills","category":"anthropic-skill","format":"markdown","content":"---\nname: builtin-skills\ndescription:\n  Use when creating a new builtin skill for Crush, editing an existing builtin\n  skill (internal/skills/builtin/), or when the user needs to understand how the\n  embedded skill system works.\n---\n\n# Builtin Skills\n\nCrush embeds skills directly into the binary via `internal/skills/builtin/`.\nThese are always available without user configuration.\n\n## How It Works\n\n- Each skill lives in `internal/skills/builtin/<skill-name>/SKILL.md`.\n- The tree is embedded at compile time via `//go:embed builtin/*` in\n  `internal/skills/embed.go`.\n- `DiscoverBuiltin()` walks the embedded FS, parses each `SKILL.md`, and sets\n  paths with the `crush://skills/` prefix (e.g., `crush://skills/jq/SKILL.md`).\n- The View tool resolves `crush://` paths from the embedded FS, not disk.\n- User skills with the same name override builtins (last occurrence wins in\n  `Deduplicate()`).\n\n## Adding a New Builtin Skill\n\n1. Create `internal/skills/builtin/<skill-name>/SKILL.md` with YAML frontmatter\n   (`name`, `description`) and markdown instructions. The directory name must\n   match the `name` field.\n2. No extra wiring needed — `//go:embed builtin/*` picks up new directories\n   automatically.\n3. Add a test assertion in `TestDiscoverBuiltin` in\n   `internal/skills/skills_test.go` to verify discovery.\n4. Build and test: `go build . && go test ./internal/skills/...`\n\n## Existing Builtin Skills\n\n| Skill          | Directory               | Description                                |\n| -------------- | ----------------------- | ------------------------------------------ |\n| `crush-config` | `builtin/crush-config/` | Crush configuration help                   |\n| `crush-hooks`  | `builtin/crush-hooks/`  | Authoring, configuring and debugging hooks |\n| `jq`           | `builtin/jq/`           | jq JSON processor usage guide              |\n","frontmatter":{"name":"builtin-skills","description":"Use when creating a new builtin skill for Crush, editing an existing builtin skill (internal/skills/builtin/), or when the user needs to understand how the embedded skill system works."},"isInternal":false,"tokens":444,"sizeBytes":1866},{"name":"SKILL.md","path":".agents/skills/shell-builtins/SKILL.md","rawUrl":"https://raw.githubusercontent.com/charmbracelet/crush/HEAD/.agents/skills/shell-builtins/SKILL.md","title":"shell-builtins","category":"anthropic-skill","format":"markdown","content":"---\nname: shell-builtins\ndescription: Use when creating a new shell builtin command for Crush (internal/shell/), editing an existing one, or when the user needs to understand how commands are intercepted in Crush's embedded shell.\n---\n\n# Shell Builtins\n\nCrush's shell (`internal/shell/`) uses `mvdan.cc/sh/v3` for POSIX shell\nemulation. Commands can be intercepted before they reach the OS by adding\n**builtins** — functions handled in-process.\n\n## How Builtins Work\n\nBuiltins live in `builtinHandler()` in `internal/shell/run.go`. This is an\n`interp.ExecHandlerFunc` middleware registered in `standardHandlers()`\n**before** the block handler, so builtins run even for commands that would\notherwise be blocked. The same handler chain is shared by the stateful\n`Shell` type and the stateless `Run` entrypoint used by the hook runner,\nso builtins are available identically in the `bash` tool and in hooks.\n\nThe handler is a switch on `args[0]`. Each case either handles the command\ninline or delegates to a helper function.\n\n## Adding a New Builtin\n\n1. **Add the case** to the switch in `builtinHandler()` in `run.go`.\n2. **Get I/O from the handler context**, not from `os.Stdin`/`os.Stdout`.\n   This ensures the builtin works with pipes and redirections:\n   ```go\n   case \"mycommand\":\n       hc := interp.HandlerCtx(ctx)\n       return handleMyCommand(ctx, args, hc.Stdin, hc.Stdout, hc.Stderr)\n   ```\n3. **Implement the handler** in its own file (e.g.,\n   `internal/shell/mycommand.go`). The function signature must accept a\n   `context.Context` as the first parameter, plus args, stdin, stdout, and\n   stderr:\n   ```go\n   func handleMyCommand(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {\n       // args[0] is the command name (\"mycommand\"), args[1:] are arguments.\n       // Write output to stdout, errors to stderr.\n       // Return nil on success, or interp.ExitStatus(n) for non-zero exit codes.\n   }\n   ```\n4. **Poll `ctx` in every unbounded loop.** Builtins that iterate over\n   input, emit values in a generator-style loop, or do any other work\n   that can exceed a few milliseconds MUST check `ctx.Err()` on each\n   iteration and return it verbatim when non-nil. Hook timeouts rely on\n   this: an unbounded builtin that never polls ctx cannot be interrupted\n   by a hook's `timeout_sec`, and the hook runner will have to abandon\n   the goroutine (see `internal/hooks/runner.go`). Returning `ctx.Err()`\n   (not `interp.ExitStatus(n)`) lets callers distinguish \"command exited\n   non-zero\" from \"we ran out of time\".\n   ```go\n   for _, item := range items {\n       if err := ctx.Err(); err != nil {\n           return err\n       }\n       // ... process item\n   }\n   ```\n5. **Return values**: return `nil` for success, `interp.ExitStatus(n)` for\n   non-zero exit codes, or `ctx.Err()` on cancellation. Write error\n   messages to `stderr` before returning.\n6. **No extra wiring needed** — `builtinHandler()` is already registered\n   in `standardHandlers()`.\n\n## Existing Builtins\n\n| Command | File | Description |\n|---------|------|-------------|\n| `jq` | `jq.go` | JSON processor using `github.com/itchyny/gojq` |\n","frontmatter":{"name":"shell-builtins","description":"Use when creating a new shell builtin command for Crush (internal/shell/), editing an existing one, or when the user needs to understand how commands are intercepted in Crush's embedded shell."},"isInternal":false,"tokens":777,"sizeBytes":3163},{"name":"SKILL.md","path":"internal/skills/builtin/crush-config/SKILL.md","rawUrl":"https://raw.githubusercontent.com/charmbracelet/crush/HEAD/internal/skills/builtin/crush-config/SKILL.md","title":"crush-config","category":"anthropic-skill","format":"markdown","content":"---\nname: crush-config\ndescription: 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.\n---\n\n# Crush Configuration\n\nCrush supports two config formats:\n\n- **`crushrc`** — a Bash script that builds config by calling Crush builtins.\n  **Preferred.** Because it is real Bash you get includes, secrets,\n  conditionals, and variables for free.\n- **`crush.json`** — static JSON. Fully supported; see\n  [Legacy JSON format](#legacy-json-format).\n\nBoth are discovered together and deep-merged. Priority (highest to lowest):\n\n1. `.crushrc` / `crushrc` / `.crush.json` / `crush.json` (project-local,\n   closer-to-cwd wins; Windows uses `.\\.crushrc` / `.\\crushrc`)\n2. `$XDG_CONFIG_HOME/crush/crushrc` or `~/.config/crush/crushrc`\n   (`%XDG_CONFIG_HOME%\\crush\\crushrc` or\n   `%USERPROFILE%\\.config\\crush\\crushrc` on Windows)\n\nData directories (`~/.local/share/crush` and `%LOCALAPPDATA%\\crush`) contain\nmachine-owned JSON state only; Crush does not discover or execute a `crushrc`\nfrom those locations.\n\nIf a directory has both `crushrc` and `crush.json`, they merge (`crushrc` wins\non conflicts) and Crush logs a warning.\n\n## crushrc at a glance\n\nA `crushrc` is a plain Bash script executed at load time with the same embedded\nshell the `bash` tool uses. It builds config by calling builtins (`provider`,\n`model`, `mcp`, `lsp`, `hook`, `permissions`, `option`). Statements run top to\nbottom; later statements win, and `remove`/`reset` operate on anything defined\nearlier or pulled in via `source`.\n\n```bash\n#!/usr/bin/env bash\n# Includes and secrets are just Bash.\nsource ~/.config/crush/shared.sh\n\nprovider add anthropic --api-key \"$ANTHROPIC_API_KEY\"\n\nmodel large anthropic/claude-sonnet-4-20250514 --max-tokens 16384\nmodel small anthropic/claude-haiku-4-20250514\n\noption skill-path ./skills\npermissions allow view ls grep edit\n```\n\nValues are ordinary Bash — quote and expand normally (`\"$VAR\"`, `$(cmd)`,\n`${VAR:?required}`). A failing `$(command)` aborts the load.\n\n`CRUSH_VERSION` is exported into the script so you can feature-detect the\nrunning Crush (it is the literal `devel` for local builds):\n\n```bash\n[[ \"$CRUSH_VERSION\" != devel ]] && lsp add gopls --command gopls\n```\n\n## Commands\n\nAll entity commands are verb-first. `remove` accepts `rm` as an alias. Booleans\naccept `true/false/1/0/yes/no`, case-insensitive.\n\n### providers\n\n```bash\nprovider add <id> [flags]    # define/update; repeated calls merge\nprovider remove <id>         # alias: rm — removes the provider and its models\n```\n\nFlags: `--name`, `--type` (`openai`, `openai-compat`, `anthropic`, or a local\ntype like `ollama`, `lmstudio`, `llamacpp`), `--api-key`, `--base-url`,\n`--disable BOOL`, `--flat-rate BOOL`, `--discover-models BOOL`,\n`--system-prompt-prefix TEXT`, `--extra-header KEY VALUE` (repeatable),\n`--extra-body JSON`, `--provider-options JSON`.\n\n```bash\nprovider add deepseek \\\n  --type openai-compat \\\n  --base-url \"https://api.deepseek.com/v1\" \\\n  --api-key \"${DEEPSEEK_API_KEY:?set DEEPSEEK_API_KEY}\"\n```\n\n### models\n\n```bash\nmodel add <provider>/<id> [flags]      # register a custom model (provider must exist)\nmodel remove <provider>/<id>           # alias: rm\nmodel large [<provider>/<id>] [flags]  # set the large slot; no arg prints it\nmodel small [<provider>/<id>] [flags]  # set the small slot; no arg prints it\n```\n\n- `<provider>/<id>` is the same form `crush models` prints. A missing slash is\n  an error. `model add` requires the provider to already exist.\n- `model add` flags: `--name`, `--context-window N`, `--default-max-tokens N`,\n  `--can-reason BOOL`, `--supports-images BOOL`, `--price-input F`,\n  `--price-output F`, `--price-cache-create F`, `--price-cache-hit F`,\n  `--reasoning-effort low|medium|high`.\n- `model large`/`model small` flags: `--think`, `--reasoning-effort`,\n  `--max-tokens N`, `--temperature F`, `--top-p F`, `--top-k N`,\n  `--frequency-penalty F`, `--presence-penalty F`, `--provider-options JSON`.\n- `model large` with no argument prints the current selection as `provider/id`,\n  usable in `$(model large)`.\n\n`large` is the primary coding model; `small` is used for summarization.\n\n### mcp\n\n```bash\nmcp add <name> --type stdio|sse|http [flags]   # default type is stdio\nmcp remove <name>                              # alias: rm\n```\n\nFlags: `--command CMD`, `--args ARG` (repeatable), `--env KEY VALUE`\n(repeatable), `--url URL`, `--header KEY VALUE` (repeatable), `--timeout N`,\n`--disabled BOOL`, `--disabled-tools TOOL` (repeatable), `--enabled-tools TOOL`\n(repeatable), `--oauth BOOL`, `--oauth-client-id ID`, `--oauth-client-secret SECRET`,\n`--oauth-callback-port PORT`.\n\n```bash\nmcp add github --type http \\\n  --url \"https://api.githubcopilot.com/mcp/\" \\\n  --header Authorization \"Bearer $GH_PAT\"\n\nmcp add filesystem --command node --args /path/to/mcp-server.js\n```\n\n### lsp\n\n```bash\nlsp add <name> --command CMD [flags]\nlsp remove <name>                     # alias: rm\n```\n\nFlags: `--args ARG` (repeatable), `--env KEY VALUE` (repeatable),\n`--filetypes TYPE` (repeatable), `--root-markers MARKER` (repeatable),\n`--timeout N`, `--disabled BOOL`, `--init-options JSON`, `--options JSON`.\n\n```bash\nlsp add go --command gopls --env GOPATH \"$HOME/go\"\nlsp add typescript --command typescript-language-server --args --stdio\n```\n\n### hooks\n\n```bash\nhook add <event> --command CMD [--name NAME] [--matcher REGEX] [--timeout N]\nhook remove <event> [--name NAME]    # alias: rm; without --name clears the event\n```\n\nOnly named hooks can be removed individually — give a hook `--name` if you\nintend to remove it later. See [Hooks runtime](#hooks-runtime) for how hooks\nexecute (stdin payload, env vars, decisions).\n\n```bash\nhook add PreToolUse --matcher \"^bash$\" --command \".crush/hooks/no-haskell.sh\" --name no-haskell\n```\n\n### permissions\n\n```bash\npermissions allow <tool> [<tool> ...]   # tools that skip permission prompts\npermissions deny <tool> [<tool> ...]    # hide tools from the agent entirely\n```\n\n`deny` is the inverse of `allow`: it writes `options.disabled_tools`. A denied\ntool is hidden from the agent, not merely prompted for.\n\n### options\n\n```bash\noption <key> [value]\noption reset <list-key>    # clear a list option back to empty\n```\n\n- **Boolean keys** (value optional, defaults `true`): `debug`, `debug-lsp`,\n  `auto-lsp`, `progress`.\n- **Boolean keys phrased positively** (stored as the negated field): `metrics`,\n  `auto-summarize`, `provider-auto-update`,\n  `default-providers`. Example: `option metrics false` disables metrics.\n- **String keys**: `data-directory`, `initialize-as`, `notifications`.\n- **Attribution keys**: `attribution-trailer-style` (`none`, `co-authored-by`,\n  `assisted-by`) and `attribution-generated-with` (boolean).\n- **UI settings**: `option ui compact BOOL`, `option ui diff unified|split`,\n  `option ui transparent BOOL`, `option ui mouse BOOL` (default `true`;\n  disable to let the terminal/tmux handle selection, copy/paste, and\n  scrolling), `option ui scrollbar default|always|never`,\n  `option ui exit-banner default|compact|none`,\n  `option ui completions-max-depth N`, `option ui completions-max-items N`.\n- **List keys** (singular, one value per call, repeatable): `context-path`,\n  `global-context-path`, `skill-path`, `disable-skill`. Use `option reset <key>`\n  to wipe inherited values (e.g. after `source`).\n\n```bash\noption progress false\noption skill-path ./skills\noption disable-skill crush-config\noption attribution-trailer-style assisted-by\noption attribution-generated-with true\noption ui compact true\noption ui diff unified\noption ui exit-banner compact\n```\n\n> [!IMPORTANT] These skill paths are loaded by default and do NOT need\n> `skill-path`: `.agents/skills`, `.crush/skills`, `.claude/skills`,\n> `.cursor/skills`.\n\n## Hooks runtime\n\nHooks are user-defined shell commands that fire on agent events. Currently only\n`PreToolUse` is supported, which runs before a tool executes. This behavior is\nthe same however the hook is defined (`hook add` or JSON).\n\n### How hooks work\n\n1. When a tool is about to be called, all `PreToolUse` hooks with a matching\n   `matcher` (or no matcher) run in parallel.\n2. Duplicate commands are deduplicated — each unique command runs at most once.\n3. The hook receives JSON on **stdin** and hook-specific **environment\n   variables**.\n\nEvent names are case-insensitive and accept snake_case: `PreToolUse`,\n`pretooluse`, `pre_tool_use`, `PRE_TOOL_USE` all work.\n\n### Hook input (stdin)\n\n```json\n{\n  \"event\": \"PreToolUse\",\n  \"session_id\": \"abc-123\",\n  \"cwd\": \"/path/to/project\",\n  \"tool_name\": \"bash\",\n  \"tool_input\": { \"command\": \"ls -la\" }\n}\n```\n\n### Hook environment variables\n\n| Variable                     | Description                                       |\n| ---------------------------- | ------------------------------------------------- |\n| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)                    |\n| `CRUSH_TOOL_NAME`            | Name of the tool being called                     |\n| `CRUSH_SESSION_ID`           | Current session ID                                |\n| `CRUSH_CWD`                  | Current working directory                         |\n| `CRUSH_PROJECT_DIR`          | Project root directory                            |\n| `CRUSH_TOOL_INPUT_COMMAND`   | Value of `command` from tool input (if present)   |\n| `CRUSH_TOOL_INPUT_FILE_PATH` | Value of `file_path` from tool input (if present) |\n\n### Hook output\n\n**Exit code 0** — hook succeeded. Stdout is parsed as JSON:\n\n```json\n{ \"decision\": \"allow\", \"context\": \"optional context appended to tool result\" }\n```\n\n- `decision`: `allow` to explicitly allow, `deny` to block, `none` (or omit).\n- `reason`: explanation (used when denying).\n- `context`: extra context appended to the tool result.\n- `updated_input`: replacement JSON for the tool input; last non-empty wins.\n\n**Exit code 2** — the tool call is blocked; stderr is the deny reason.\n\n**Any other exit code** — non-blocking error; the tool call proceeds.\n\n### Decision aggregation\n\n- **Deny wins over allow** — any deny blocks the call.\n- **Allow wins over none** — a lone allow lets it proceed.\n- Deny reasons and context strings are concatenated (newline-separated).\n- For `updated_input`, the last non-empty value wins.\n\n### Claude Code compatibility\n\nCrush also accepts the Claude Code hook output format, so existing hooks work\nunchanged:\n\n```json\n{\n  \"hookSpecificOutput\": {\n    \"permissionDecision\": \"allow\",\n    \"permissionDecisionReason\": \"Auto-approved\",\n    \"updatedInput\": { \"command\": \"echo rewritten\" }\n  }\n}\n```\n\n## User-invocable skills\n\nSkills can be invoked as commands. Add `user-invocable: true` to the skill's\nYAML frontmatter:\n\n```yaml\n---\nname: my-skill\ndescription: A skill that can be invoked as a command.\nuser-invocable: true\n---\n```\n\n- Global skills appear as `user:skill-name`; project skills as\n  `project:skill-name`.\n- Add `disable-model-invocation: true` to keep a skill user-only (hidden from\n  the model's available-skills list but still manually invocable).\n\n## Environment variables\n\n- `CRUSH_VERSION` — exported into `crushrc` at load; the running version (or\n  `devel` for local builds).\n- `CRUSH_GLOBAL_CONFIG` — override global config location.\n- `CRUSH_GLOBAL_DATA` — override data directory location.\n- `CRUSH_SKILLS_DIR` — override default skills directory.\n\n## Legacy JSON format\n\n`crush.json` is the original static format. It still works and merges with\n`crushrc`. Basic structure:\n\n```json\n{\n  \"$schema\": \"https://charm.land/crush.json\",\n  \"models\": {},\n  \"providers\": {},\n  \"mcp\": {},\n  \"lsp\": {},\n  \"hooks\": {},\n  \"options\": {},\n  \"permissions\": {}\n}\n```\n\nThe `$schema` property enables IDE autocomplete but is optional.\n\n### crushrc ↔ crush.json mapping\n\n| crushrc                             | crush.json                                             |\n| ------------------------------------ | ------------------------------------------------------ |\n| `provider add openai --api-key \"$K\"` | `providers.openai = {\"api_key\": \"$K\"}`                 |\n| `model add openai/gpt-x --name X`    | append to `providers.openai.models[]`                  |\n| `model large openai/gpt-x`           | `models.large = {\"provider\":\"openai\",\"model\":\"gpt-x\"}` |\n| `mcp add gh --type http --url U`     | `mcp.gh = {\"type\":\"http\",\"url\":\"U\"}`                   |\n| `lsp add go --command gopls`         | `lsp.go = {\"command\":\"gopls\"}`                         |\n| `hook add PreToolUse --command C`    | append to `hooks.PreToolUse[]`                         |\n| `permissions allow view ls`          | `permissions.allowed_tools = [\"view\",\"ls\"]`            |\n| `permissions deny bash`              | `options.disabled_tools = [\"bash\"]`                    |\n| `option skill-path ./skills`         | `options.skills_paths = [\"./skills\"]`                  |\n| `option metrics false`               | `options.disable_metrics = true`                       |\n| `option request-timeout 300`          | `options.request_timeout = 300`                        |\n| `option max-background-jobs 200`     | `options.max_background_jobs = 200`                    |\n| `option attribution-trailer-style none` | `options.attribution.trailer_style = \"none\"`        |\n| `option attribution-generated-with false` | `options.attribution.generated_with = false`       |\n\n### Shell expansion in crush.json\n\nIn JSON, only selected string fields are run through the embedded shell at load\ntime (in `crushrc`, everything is native Bash so this table does not apply):\n\n| Surface                                                         | Expansion                          |\n| --------------------------------------------------------------- | ---------------------------------- |\n| Provider `api_key`, `base_url`, `api_endpoint`, `extra_headers` | yes                                |\n| Provider `extra_body`                                           | **no** (JSON passthrough)          |\n| MCP `command`, `args`, `env`, `headers`, `url`                  | yes                                |\n| LSP `command`, `args`, `env`                                    | yes                                |\n| Hook `command`                                                  | runs via `sh -c`, not the resolver |\n\nSupported constructs: `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:+alt}`,\n`${VAR:?message}`, `$(command)`. An unset variable expands to empty; a failing\n`$(command)` is a hard error. A header that resolves to empty is dropped from\nthe request.\n\n### Security note\n\nBoth formats are trusted code. `crushrc` runs entirely, and any `$(...)` in\n`crush.json` runs at load time, with the invoking user's shell privileges,\nbefore the UI appears. Don't launch Crush in a directory whose config you\nhaven't reviewed.\n","frontmatter":{"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."},"isInternal":false,"tokens":3728,"sizeBytes":14884},{"name":"SKILL.md","path":"internal/skills/builtin/crush-hooks/SKILL.md","rawUrl":"https://raw.githubusercontent.com/charmbracelet/crush/HEAD/internal/skills/builtin/crush-hooks/SKILL.md","title":"crush-hooks","category":"anthropic-skill","format":"markdown","content":"---\nname: crush-hooks\ndescription: Use when the user wants to add, write, debug, or configure a Crush hook — gating or blocking tool calls, approving or rewriting tool input before execution, injecting context into tool results, or troubleshooting hook behavior in crush.json.\n---\n\n# Crush Hooks\n\nHooks are user-defined commands in `crush.json` that fire at specific points\nduring execution, giving deterministic control over tool behavior. They run\n**before** permission checks and **only on the top-level agent's** tool calls —\nsub-agent calls (task tool, agentic_fetch, etc.) are not intercepted, though\nthe sub-agent tool call itself is.\n\nFor the full reference, see `docs/hooks/README.md`. This skill covers what you\nneed to author correct hooks.\n\n## Supported Events\n\nOnly `PreToolUse` is currently supported. Event names are case-insensitive and\naccept snake_case (`PreToolUse`, `pretooluse`, `pre_tool_use` all work).\n\n## Configuration\n\n```jsonc\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"^bash$\",              // regex against tool name (optional; omit to match all)\n        \"command\": \"./hooks/my-hook.sh\",   // required: shell command to run\n        \"timeout\": 10                     // optional: seconds, default 30\n      }\n    ]\n  }\n}\n```\n\nProject-level hooks take precedence over global. Matching hooks are deduped by\n`command`, run in parallel, and aggregated in **config order** (not finish order).\n\n## Language\n\n`command` is a shell command, so hooks can be written in any language by\ninvoking the interpreter: `node ./hooks/h.js`, `python3 ./hooks/h.py`,\n`./hooks/h.sh`, inline `echo '…'`, etc. The rest of this skill shows bash, but\nthe input/output contract is identical regardless of language.\n\n## Input\n\n**Environment variables:**\n\n| Variable                     | Description                              |\n| ---------------------------- | ---------------------------------------- |\n| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)           |\n| `CRUSH_TOOL_NAME`            | Tool being called (e.g. `bash`)          |\n| `CRUSH_SESSION_ID`           | Current session ID                       |\n| `CRUSH_CWD`                  | Working directory                        |\n| `CRUSH_PROJECT_DIR`          | Project root directory                   |\n| `CRUSH_TOOL_INPUT_COMMAND`   | For `bash` calls: the shell command      |\n| `CRUSH_TOOL_INPUT_FILE_PATH` | For file tools: the target file path     |\n\n**JSON on stdin:**\n\n```json\n{\n  \"event\": \"PreToolUse\",\n  \"session_id\": \"313909e\",\n  \"cwd\": \"/home/user/project\",\n  \"tool_name\": \"bash\",\n  \"tool_input\": {\"command\": \"rm -rf /\"}\n}\n```\n\n## Output\n\nCommunicate back via exit code (+ stderr) or JSON on stdout.\n\n| Exit Code | Meaning                                                       |\n| --------- | ------------------------------------------------------------- |\n| 0         | Success. Stdout is parsed as the JSON envelope below.         |\n| 2         | Block this tool call. Stderr becomes the deny reason.         |\n| 49        | Halt the whole turn. Stderr becomes the halt reason.          |\n| Other     | Non-blocking error. Logged and ignored; tool call proceeds.   |\n\nExit 2 blocks one tool call (agent sees the reason and can try again); exit 49\nends the whole turn (user takes over). Default to deny — reach for halt only\nwhen letting the agent retry is itself the problem (e.g. secrets detected,\npolicy violation).\n\n**JSON envelope (exit 0):**\n\n```json\n{\n  \"version\": 1,\n  \"decision\": \"allow\",\n  \"halt\": false,\n  \"reason\": \"...\",\n  \"context\": \"Extra info for the model\",\n  \"updated_input\": {\"command\": \"rewritten\"}\n}\n```\n\n- **`decision`**: `\"allow\"`, `\"deny\"`, or omit. `\"allow\"` is **affirmative\n  pre-approval** — it bypasses the permission prompt entirely. Omit it\n  (or `null`) when you only want to inject context or rewrite input without\n  also auto-approving the call.\n- **`halt: true`**: ends the turn (same as exit 49).\n- **`reason`**: shown to the model on deny; to model and user on halt.\n- **`context`**: string **or array of strings**. Appended to what the model\n  sees. Empty entries are dropped.\n- **`updated_input`**: **shallow-merge patch** against `tool_input`, not a\n  replacement. Keys you include overwrite; keys you don't are preserved.\n  Nested objects are replaced wholesale, not deep-merged. Ignored on deny/halt.\n\n## Aggregation (Multiple Hooks)\n\nComposed in **config order**:\n\n- `deny` > `allow` > no opinion. First deny decides; subsequent allows don't override.\n- `halt` is sticky: any hook halting ends the turn.\n- `reason` and `context` concatenate in config order (newline-joined).\n- `updated_input` patches shallow-merge sequentially; later patches win on colliding keys.\n\n## Canonical Examples\n\n### Block destructive commands\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nif echo \"$CRUSH_TOOL_INPUT_COMMAND\" | grep -qE 'rm\\s+-(rf|fr)\\s+/'; then\n  echo \"Refusing to run rm -rf against root\" >&2\n  exit 2\nfi\n```\n\nConfig: `{\"matcher\": \"^bash$\", \"command\": \"./hooks/no-rm-rf.sh\"}`\n\n### Auto-approve read-only tools (inline, no script)\n\n```jsonc\n{\"matcher\": \"^(view|ls|grep|glob)$\", \"command\": \"echo '{\\\"decision\\\":\\\"allow\\\"}'\"}\n```\n\nEvery `view`/`ls`/`grep`/`glob` call now runs without prompting.\n\n### Inject context without auto-approving\n\nEmit only `context` — omit `decision` so the normal permission flow still runs.\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nif [[ \"$CRUSH_TOOL_INPUT_FILE_PATH\" == *.go ]]; then\n  echo '{\"context\": \"Remember: run gofumpt after editing Go files.\"}'\nelse\n  echo '{}'\nfi\n```\n\nConfig: `{\"matcher\": \"^(edit|write|multiedit)$\", \"command\": \"./hooks/go-context.sh\"}`\n\n### Rewrite tool input (shallow merge)\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nread -r input\nrewritten=$(echo \"$input\" | jq -r '.tool_input.command' | some-rewriter)\n\ncat <<EOF\n{\n  \"context\": \"Rewrote command\",\n  \"updated_input\": {\"command\": \"$rewritten\"}\n}\nEOF\n```\n\nIf the original call was `{\"command\": \"npm test\", \"timeout\": 60000}`, the\ntool runs with `{\"command\": \"<rewritten>\", \"timeout\": 60000}` — `timeout` is\npreserved.\n\n## Authoring Checklist\n\n1. Add `#!/usr/bin/env bash` and `set -euo pipefail` (for shell scripts).\n2. `chmod +x` the script.\n3. Add the entry under `hooks.PreToolUse` in `crush.json` with the right matcher.\n4. Decide intent: inject context (omit `decision`), auto-approve (`\"allow\"`),\n   block (`exit 2`), or halt (`exit 49`).\n5. If rewriting input, remember `updated_input` is a shallow merge — only\n   include the keys you want to change.\n\n## Debugging\n\n- Timeouts kill the hook silently and the tool call proceeds. Bump `timeout` if needed.\n- Non-zero exit codes other than 2/49 are logged but don't block — check Crush logs.\n- Use `echo \"debug info\" >&2` for logging without corrupting stdout JSON.\n- `matcher` is a regex against the tool name. Use `^bash$` (not `bash`) if you\n  don't also want to match `mcp_something_bash`.\n\n## Claude Code Compatibility\n\nCrush also accepts Claude Code's `hookSpecificOutput` envelope. One intentional\ndivergence: Crush treats `updated_input` as shallow-merge, Claude Code replaces.\nExisting Claude Code hooks work without modification for the matcher/decision\nparts; revisit any that relied on `updatedInput` fully replacing tool input.\n","frontmatter":{"name":"crush-hooks","description":"Use when the user wants to add, write, debug, or configure a Crush hook — gating or blocking tool calls, approving or rewriting tool input before execution, injecting context into tool results, or troubleshooting hook behavior in crush.json."},"isInternal":false,"tokens":1841,"sizeBytes":7289},{"name":"SKILL.md","path":"internal/skills/builtin/jq/SKILL.md","rawUrl":"https://raw.githubusercontent.com/charmbracelet/crush/HEAD/internal/skills/builtin/jq/SKILL.md","title":"jq","category":"anthropic-skill","format":"markdown","content":"---\nname: jq\ndescription: Use when the user needs to query, filter, reshape, extract, create, or construct JSON data — including API responses, config files, log output, or any structured data — or when helping the user write or debug JSON transformations.\n---\n\n# jq — Built-in JSON Processor\n\nCrush ships a built-in `jq` command (via `github.com/itchyny/gojq`) available\nin the bash tool. No external binary is required.\n\n## Supported Flags\n\n| Flag | Description |\n|------|-------------|\n| `-r`, `--raw-output` | Output strings without quotes |\n| `-j`, `--join-output` | Like `-r` but no trailing newline |\n| `-c`, `--compact-output` | One-line JSON output |\n| `-s`, `--slurp` | Read all inputs into an array |\n| `-n`, `--null-input` | Use `null` as input (ignore stdin) |\n| `-e`, `--exit-status` | Exit 1 if last output is `false` or `null` |\n| `-R`, `--raw-input` | Read each line as a string, not JSON |\n| `--arg name value` | Bind `$name` to a string value |\n| `--argjson name value` | Bind `$name` to a parsed JSON value |\n\nFile arguments after the filter are also supported: `jq '.foo' file.json`.\n\n## Differences from Standard jq\n\nThe built-in uses gojq, which is a pure-Go jq implementation. Key\ndifferences:\n\n- **No object key ordering** — keys are sorted by default; `keys_unsorted`\n  and `-S` are unavailable.\n- **Arbitrary-precision integers** — large integers keep full precision\n  (addition, subtraction, multiplication, modulo, division when divisible).\n- **String indexing** — `\"abcde\"[2]` returns `\"c\"`.\n- **Not supported** — `--ascii-output`, `--seq`, `--stream`,\n  `--stream-errors`, `-f`/`--from-file`, `--slurpfile`, `--rawfile`,\n  `--args`, `--jsonargs`, `input_line_number`, `$__loc__`, some regex\n  features (backreferences, look-around).\n- **YAML** — gojq supports `--yaml-input`/`--yaml-output` but the\n  built-in does not currently expose these flags.\n\n## Common Patterns\n\nExtract a field:\n```sh\necho '{\"name\":\"crush\"}' | jq '.name'\n```\n\nFilter an array:\n```sh\necho '[1,2,3,4,5]' | jq '[.[] | select(. > 3)]'\n```\n\nReshape objects:\n```sh\necho '{\"first\":\"Ada\",\"last\":\"Lovelace\"}' | jq '{full: (.first + \" \" + .last)}'\n```\n\nUse variables:\n```sh\necho '{}' | jq --arg host localhost --argjson port 8080 '{host: $host, port: $port}'\n```\n\nSlurp multiple JSON values:\n```sh\necho '{\"a\":1}{\"b\":2}' | jq -s '.'\n```\n\nCompact output for piping:\n```sh\necho '{\"a\":1}' | jq -c '.a += 1'\n```\n\nRaw string output:\n```sh\necho '[\"one\",\"two\",\"three\"]' | jq -r '.[]'\n```\n\nProcess a file:\n```sh\njq '.dependencies | keys' package.json\n```\n\nNull input for constructing JSON:\n```sh\njq -n --arg msg hello '{\"message\": $msg}'\n```\n\n## Tips\n\n- Pipe jq output to other commands: `jq -r '.url' data.json | xargs curl`\n- Chain filters with `|` inside the expression, not shell pipes.\n- Use `try` to suppress errors on missing keys: `jq 'try .foo.bar'`\n- Use `// \"default\"` for fallback values: `jq '.name // \"unknown\"'`\n- Use `@csv`, `@tsv`, `@base64`, `@html`, `@uri` for format strings.\n","frontmatter":{"name":"jq","description":"Use when the user needs to query, filter, reshape, extract, create, or construct JSON data — including API responses, config files, log output, or any structured data — or when helping the user write or debug JSON transformations."},"isInternal":false,"tokens":873,"sizeBytes":2999}],"systemPromptSnippet":"<yakaai_skills repo=\"charmbracelet/crush\">\n<!-- File: AGENTS.md (Tokens: ~2486 | Category: root-instruction) -->\n# Crush Development Guide\n\n## Project Overview\n\nCrush is a terminal-based AI coding assistant built in Go by\n[Charm](https://charm.land). It connects to LLMs and gives them tools to read,\nwrite, and execute code. It supports multiple providers (Anthropic, OpenAI,\nGemini, Bedrock, Copilot, Hyper, MiniMax, Vercel, and more), integrates with\nLSPs for code intelligence, and supports extensibility via MCP servers and\nagent skills.\n\nThe module path is `github.com/charmbracelet/crush`.\n\n## Architecture\n\n```\nmain.go                            CLI entry point (cobra via internal/cmd)\ninternal/\n  app/app.go                       Top-level wiring: DB, config, agents, LSP, MCP, events\n  cmd/                             CLI commands (root, run, login, models, stats, sessions)\n  config/\n    config.go                      Config struct, context file paths, agent definitions\n    load.go                        crushrc and crush.json loading and validation\n    provider.go                    Provider configuration and model resolution\n  shellconfig/                      Bash-powered config format (crushrc builtins)\n  agent/\n    agent.go                       SessionAgent: runs LLM conversations per session\n    coordinator.go                 Coordinator: manages named agents (\"coder\", \"task\")\n    hooked_tool.go                 Decorator that runs PreToolUse hooks before tool execution\n    prompts.go                     Loads Go-template system prompts\n    templates/                     System prompt templates (coder.md.tpl, task.md.tpl, etc.)\n    tools/                         All built-in tools (bash, edit, view, grep, glob, etc.)\n      mcp/                         MCP client integration\n  hooks/                           Hook engine: runs user shell commands on hook events\n    hooks.go                       Decision types, aggregation logic, event constants\n    runner.go                      Parallel hook execution, timeout, dedup\n    input.go                       Stdin payload builder, env vars, stdout parsing (Crush + Claude Code compat)\n  session/session.go               Session CRUD backed by SQLite\n  message/                         Message model and content types\n  db/                              SQLite via sqlc, with migrations\n    sql/                           Raw SQL queries (consumed by sqlc)\n    migrations/                    Schema migrations\n  lsp/                             LSP client manager, auto-discovery, on-demand startup\n  ui/                              Bubble Tea v2 TUI (see internal/ui/AGENTS.md)\n  permission/                      Tool permission checking and allow-lists\n  skills/                          Skill file discovery and loading\n  shell/                           Bash command execution with background job support\n  event/                           Telemetry (PostHog)\n  pubsub/                          Internal pub/sub for cross-component messaging\n  filetracker/                     Tracks files touched per session\n  history/                         Prompt history\n```\n\n### Key Dependency Roles\n\n- **`charm.land/fantasy`**: LLM provider abstraction layer. Handles protocol\n  differences between Anthropic, OpenAI, Gemini, etc. Used in `internal/app`\n  and `internal/agent`.\n- **`charm.land/bubbletea/v2`**: TUI framework powering the interactive UI.\n- **`charm.land/lipgloss/v2`**: Terminal styling.\n- **`charm.land/glamour/v2`**: Markdown rendering in the terminal.\n- **`charm.land/catwalk`**: Snapshot/golden-file testing for TUI components.\n- **`sqlc`**: Generates Go code from SQL queries in `internal/db/sql/`.\n\n### Key Patterns\n\n- **Config is a Service**: accessed via `config.Service`, not global state.\n- **Tools are self-documenting**: each tool has a `.go` implementation and a\n  `.md` description file in `internal/agent/tools/`.\n- **System prompts are Go templates**: `internal/agent/templates/*.md.tpl`\n  with runtime data injected.\n- **Context files**: Crush reads AGENTS.md, CRUSH.md, CLAUDE.md, GEMINI.md\n  (and `.local` variants) from the working directory for project-specific\n  instructions.\n- **Bash config format**: Crush's primary config format is `crushrc` — a\n  Bash script using builtins (`provider`, `model`, `mcp`, `lsp`,\n  `permissions`, `hook`, `options`) to define config. `crush.json` is still\n  supported but is deprecated in favor of `crushrc` and may be removed in a\n  future release. Shell config files are discovered alongside JSON configs\n  and deep-merged through the same pipeline. Builtins are registered via\n  `shell.RegisterBuiltin` and gated by a `ConfigBuilder` on the context —\n  they are no-ops during normal bash tool execution. See\n  `internal/shellconfig/`.\n- **Persistence**: SQLite + sqlc. All queries live in `internal/db/sql/`,\n  generated code in `internal/db/`. Migrations in `internal/db/migrations/`.\n- **Pub/sub**: `internal/pubsub` for decoupled communication between agent,\n  UI, and services.\n- **Hooks**: User-defined shell commands in `crushrc` (or `crush.json`)\n  that fire before tool execution. The engine (`internal/hooks/`) is\n  independent of fantasy and agent — it takes inputs, runs commands,\n  returns decisions. The `hookedTool` decorator in\n  `internal/agent/hooked_tool.go` wraps tools at the coordinator level.\n  Hooks run before permission checks. See `HOOKS.md` for the user-facing\n  protocol.\n- **CGO disabled**: builds with `CGO_ENABLED=0` and\n  `GOEXPERIMENT=greenteagc`.\n\n## Build/Test/Lint Commands\n\n- **Build**: `go build .` or `go run .`\n- **Test**: `task test` or `go test ./...` (run single test:\n  `go test ./internal/llm/prompt -run TestGetContextFromPaths`)\n- **Update Golden Files**: `go test ./... -update` (regenerates `.golden`\n  files when test output changes)\n  - Update specific package:\n    `go test ./internal/tui/components/core -update` (in this case,\n    we're updating \"core\")\n- **Lint**: `task lint:fix`\n- **Format**: `task fmt` (`gofumpt -w .`)\n- **Modernize**: `task modernize` (runs `modernize` which makes code\n  simplifications)\n- **Dev**: `task dev` (runs with profiling enabled)\n\n## Code Style Guidelines\n\n- **Imports**: Use `goimports` formatting, group stdlib, external, internal\n  packages.\n- **Formatting**: Use gofumpt (stricter than gofmt), enabled in\n  golangci-lint.\n- **Naming**: Standard Go conventions — PascalCase for exported, camelCase\n  for unexported.\n- **Types**: Prefer explicit types, use type aliases for clarity (e.g.,\n  `type AgentName string`).\n- **Error handling**: Return errors explicitly, use `fmt.Errorf` for\n  wrapping.\n- **Context**: Always pass `context.Context` as first parameter for\n  operations.\n- **Interfaces**: Define interfaces in consuming packages, keep them small\n  and focused.\n- **Structs**: Use struct embedding for composition, group related fields.\n- **Constants**: Use typed constants with iota for enums, group in const\n  blocks.\n- **Testing**: Use testify's `require` package, parallel tests with\n  `t.Parallel()`, `t.SetEnv()` to set environment variables. Always use\n  `t.Tempdir()` when in need of a temporary directory. This directory does\n  not need to be removed.\n- **JSON tags**: Use snake_case for JSON field names.\n- **File permissions**: Use octal notation (0o755, 0o644) for file\n  permissions.\n- **Log messages**: Log messages must start with a capital letter (e.g.,\n  \"Failed to save session\" not \"failed to save session\").\n  - This is enforced by `task lint:log` which runs as part of `task lint`.\n- **Comments**: End comments in periods unless comments are at the end of the\n  line.\n\n## Testing with Mock Providers\n\nWhen writing tests that involve provider configurations, use the mock\nproviders to avoid API calls:\n\n```go\nfunc TestYourFunction(t *testing.T) {\n    // Enable mock providers for testing\n    originalUseMock := config.UseMockProviders\n    config.UseMockProviders = true\n    defer func() {\n        config.UseMockProviders = originalUseMock\n        config.ResetProviders()\n    }()\n\n    // Reset providers to ensure fresh mock data\n    config.ResetProviders()\n\n    // Your test code here - providers will now return mock data\n    providers := config.Providers()\n    // ... test logic\n}\n```\n\n## Formatting\n\n- ALWAYS format any Go code you write.\n  - First, try `gofumpt -w .`.\n  - If `gofumpt` is not available, use `goimports`.\n  - If `goimports` is not available, use `gofmt`.\n  - You can also use `task fmt` to run `gofumpt -w .` on the entire project,\n    as long as `gofumpt` is on the `PATH`.\n\n## Comments\n\n- Comments that live on their own lines should start with capital letters and\n  end with periods. Wrap comments at 78 columns.\n\n## Committing\n\n- ALWAYS use semantic commits (`fix:`, `feat:`, `chore:`, `refactor:`,\n  `docs:`, `sec:`, etc).\n- Try to keep commits to one line, not including your attribution. Only use\n  multi-line commits when additional context is truly necessary.\n\n## Working on the TUI (UI)\n\nAnytime you need to work on the TUI, read `internal/ui/AGENTS.md` before\nstarting work.\n\n## Styling System\n\nThe styling system lives in `internal/ui/styles/` and is organized into\nthree layers:\n\n- **`quickstyle.go`**: The stable base theme builder. `quickStyle(opts)`\n  constructs a `Styles` struct from `quickStyleOpts` — a palette of\n  design tokens (primary, secondary, fgBase, bgBase, success, error, etc.).\n  `quickStyle` must be fully token-driven: never hardcode specific\n  `charmtone.*` colors here (except Chroma syntax highlighting, which is\n  pending tokenization). This lets any theme reuse the base without\n  inheriting Charmtone-specific colors.\n- **`themes.go`**: Defines concrete themes. Each theme function (e.g.\n  `CharmtonePantera`) calls `quickStyle` with its palette, then applies\n  theme-specific overrides as needed.\n- **`styles.go`**: Defines the `Styles` struct and its documentation —\n  the shape of what `quickStyle` produces.\n\n**Adding theme-specific overrides**: When a style genuinely needs a\ncolor that doesn't fit the token model (e.g. the bang prompt uses\nSalt/Hazy/Larple), keep `quickStyle` on the closest semantic token and\noverride only the differing colors in the theme function:\n\n```go\nfunc CharmtonePantera() Styles {\n\ts := quickStyle(quickStyleOpts{ /* palette */ })\n\n\t// Override only the colors that differ from the token defaults.\n\ts.Editor.PromptBangIconFocused = s.Editor.PromptBangIconFocused.\n\t\tForeground(charmtone.Salt).\n\t\tBackground(charmtone.Hazy)\n\n\treturn s\n}\n```\n\n**Adding a new theme**: Add a function in `themes.go` that returns the\nresult of `quickStyle` with a `quickStyleOpts` palette (plus any needed\noverrides), then wire it into `ThemeForProvider`.\n\n<!-- File: .agents/skills/builtin-skills/SKILL.md (Tokens: ~444 | Category: anthropic-skill) -->\n---\nname: builtin-skills\ndescription:\n  Use when creating a new builtin skill for Crush, editing an existing builtin\n  skill (internal/skills/builtin/), or when the user needs to understand how the\n  embedded skill system works.\n---\n\n# Builtin Skills\n\nCrush embeds skills directly into the binary via `internal/skills/builtin/`.\nThese are always available without user configuration.\n\n## How It Works\n\n- Each skill lives in `internal/skills/builtin/<skill-name>/SKILL.md`.\n- The tree is embedded at compile time via `//go:embed builtin/*` in\n  `internal/skills/embed.go`.\n- `DiscoverBuiltin()` walks the embedded FS, parses each `SKILL.md`, and sets\n  paths with the `crush://skills/` prefix (e.g., `crush://skills/jq/SKILL.md`).\n- The View tool resolves `crush://` paths from the embedded FS, not disk.\n- User skills with the same name override builtins (last occurrence wins in\n  `Deduplicate()`).\n\n## Adding a New Builtin Skill\n\n1. Create `internal/skills/builtin/<skill-name>/SKILL.md` with YAML frontmatter\n   (`name`, `description`) and markdown instructions. The directory name must\n   match the `name` field.\n2. No extra wiring needed — `//go:embed builtin/*` picks up new directories\n   automatically.\n3. Add a test assertion in `TestDiscoverBuiltin` in\n   `internal/skills/skills_test.go` to verify discovery.\n4. Build and test: `go build . && go test ./internal/skills/...`\n\n## Existing Builtin Skills\n\n| Skill          | Directory               | Description                                |\n| -------------- | ----------------------- | ------------------------------------------ |\n| `crush-config` | `builtin/crush-config/` | Crush configuration help                   |\n| `crush-hooks`  | `builtin/crush-hooks/`  | Authoring, configuring and debugging hooks |\n| `jq`           | `builtin/jq/`           | jq JSON processor usage guide              |\n\n<!-- File: .agents/skills/shell-builtins/SKILL.md (Tokens: ~777 | Category: anthropic-skill) -->\n---\nname: shell-builtins\ndescription: Use when creating a new shell builtin command for Crush (internal/shell/), editing an existing one, or when the user needs to understand how commands are intercepted in Crush's embedded shell.\n---\n\n# Shell Builtins\n\nCrush's shell (`internal/shell/`) uses `mvdan.cc/sh/v3` for POSIX shell\nemulation. Commands can be intercepted before they reach the OS by adding\n**builtins** — functions handled in-process.\n\n## How Builtins Work\n\nBuiltins live in `builtinHandler()` in `internal/shell/run.go`. This is an\n`interp.ExecHandlerFunc` middleware registered in `standardHandlers()`\n**before** the block handler, so builtins run even for commands that would\notherwise be blocked. The same handler chain is shared by the stateful\n`Shell` type and the stateless `Run` entrypoint used by the hook runner,\nso builtins are available identically in the `bash` tool and in hooks.\n\nThe handler is a switch on `args[0]`. Each case either handles the command\ninline or delegates to a helper function.\n\n## Adding a New Builtin\n\n1. **Add the case** to the switch in `builtinHandler()` in `run.go`.\n2. **Get I/O from the handler context**, not from `os.Stdin`/`os.Stdout`.\n   This ensures the builtin works with pipes and redirections:\n   ```go\n   case \"mycommand\":\n       hc := interp.HandlerCtx(ctx)\n       return handleMyCommand(ctx, args, hc.Stdin, hc.Stdout, hc.Stderr)\n   ```\n3. **Implement the handler** in its own file (e.g.,\n   `internal/shell/mycommand.go`). The function signature must accept a\n   `context.Context` as the first parameter, plus args, stdin, stdout, and\n   stderr:\n   ```go\n   func handleMyCommand(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {\n       // args[0] is the command name (\"mycommand\"), args[1:] are arguments.\n       // Write output to stdout, errors to stderr.\n       // Return nil on success, or interp.ExitStatus(n) for non-zero exit codes.\n   }\n   ```\n4. **Poll `ctx` in every unbounded loop.** Builtins that iterate over\n   input, emit values in a generator-style loop, or do any other work\n   that can exceed a few milliseconds MUST check `ctx.Err()` on each\n   iteration and return it verbatim when non-nil. Hook timeouts rely on\n   this: an unbounded builtin that never polls ctx cannot be interrupted\n   by a hook's `timeout_sec`, and the hook runner will have to abandon\n   the goroutine (see `internal/hooks/runner.go`). Returning `ctx.Err()`\n   (not `interp.ExitStatus(n)`) lets callers distinguish \"command exited\n   non-zero\" from \"we ran out of time\".\n   ```go\n   for _, item := range items {\n       if err := ctx.Err(); err != nil {\n           return err\n       }\n       // ... process item\n   }\n   ```\n5. **Return values**: return `nil` for success, `interp.ExitStatus(n)` for\n   non-zero exit codes, or `ctx.Err()` on cancellation. Write error\n   messages to `stderr` before returning.\n6. **No extra wiring needed** — `builtinHandler()` is already registered\n   in `standardHandlers()`.\n\n## Existing Builtins\n\n| Command | File | Description |\n|---------|------|-------------|\n| `jq` | `jq.go` | JSON processor using `github.com/itchyny/gojq` |\n\n<!-- File: internal/skills/builtin/crush-config/SKILL.md (Tokens: ~3728 | Category: anthropic-skill) -->\n---\nname: crush-config\ndescription: 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.\n---\n\n# Crush Configuration\n\nCrush supports two config formats:\n\n- **`crushrc`** — a Bash script that builds config by calling Crush builtins.\n  **Preferred.** Because it is real Bash you get includes, secrets,\n  conditionals, and variables for free.\n- **`crush.json`** — static JSON. Fully supported; see\n  [Legacy JSON format](#legacy-json-format).\n\nBoth are discovered together and deep-merged. Priority (highest to lowest):\n\n1. `.crushrc` / `crushrc` / `.crush.json` / `crush.json` (project-local,\n   closer-to-cwd wins; Windows uses `.\\.crushrc` / `.\\crushrc`)\n2. `$XDG_CONFIG_HOME/crush/crushrc` or `~/.config/crush/crushrc`\n   (`%XDG_CONFIG_HOME%\\crush\\crushrc` or\n   `%USERPROFILE%\\.config\\crush\\crushrc` on Windows)\n\nData directories (`~/.local/share/crush` and `%LOCALAPPDATA%\\crush`) contain\nmachine-owned JSON state only; Crush does not discover or execute a `crushrc`\nfrom those locations.\n\nIf a directory has both `crushrc` and `crush.json`, they merge (`crushrc` wins\non conflicts) and Crush logs a warning.\n\n## crushrc at a glance\n\nA `crushrc` is a plain Bash script executed at load time with the same embedded\nshell the `bash` tool uses. It builds config by calling builtins (`provider`,\n`model`, `mcp`, `lsp`, `hook`, `permissions`, `option`). Statements run top to\nbottom; later statements win, and `remove`/`reset` operate on anything defined\nearlier or pulled in via `source`.\n\n```bash\n#!/usr/bin/env bash\n# Includes and secrets are just Bash.\nsource ~/.config/crush/shared.sh\n\nprovider add anthropic --api-key \"$ANTHROPIC_API_KEY\"\n\nmodel large anthropic/claude-sonnet-4-20250514 --max-tokens 16384\nmodel small anthropic/claude-haiku-4-20250514\n\noption skill-path ./skills\npermissions allow view ls grep edit\n```\n\nValues are ordinary Bash — quote and expand normally (`\"$VAR\"`, `$(cmd)`,\n`${VAR:?required}`). A failing `$(command)` aborts the load.\n\n`CRUSH_VERSION` is exported into the script so you can feature-detect the\nrunning Crush (it is the literal `devel` for local builds):\n\n```bash\n[[ \"$CRUSH_VERSION\" != devel ]] && lsp add gopls --command gopls\n```\n\n## Commands\n\nAll entity commands are verb-first. `remove` accepts `rm` as an alias. Booleans\naccept `true/false/1/0/yes/no`, case-insensitive.\n\n### providers\n\n```bash\nprovider add <id> [flags]    # define/update; repeated calls merge\nprovider remove <id>         # alias: rm — removes the provider and its models\n```\n\nFlags: `--name`, `--type` (`openai`, `openai-compat`, `anthropic`, or a local\ntype like `ollama`, `lmstudio`, `llamacpp`), `--api-key`, `--base-url`,\n`--disable BOOL`, `--flat-rate BOOL`, `--discover-models BOOL`,\n`--system-prompt-prefix TEXT`, `--extra-header KEY VALUE` (repeatable),\n`--extra-body JSON`, `--provider-options JSON`.\n\n```bash\nprovider add deepseek \\\n  --type openai-compat \\\n  --base-url \"https://api.deepseek.com/v1\" \\\n  --api-key \"${DEEPSEEK_API_KEY:?set DEEPSEEK_API_KEY}\"\n```\n\n### models\n\n```bash\nmodel add <provider>/<id> [flags]      # register a custom model (provider must exist)\nmodel remove <provider>/<id>           # alias: rm\nmodel large [<provider>/<id>] [flags]  # set the large slot; no arg prints it\nmodel small [<provider>/<id>] [flags]  # set the small slot; no arg prints it\n```\n\n- `<provider>/<id>` is the same form `crush models` prints. A missing slash is\n  an error. `model add` requires the provider to already exist.\n- `model add` flags: `--name`, `--context-window N`, `--default-max-tokens N`,\n  `--can-reason BOOL`, `--supports-images BOOL`, `--price-input F`,\n  `--price-output F`, `--price-cache-create F`, `--price-cache-hit F`,\n  `--reasoning-effort low|medium|high`.\n- `model large`/`model small` flags: `--think`, `--reasoning-effort`,\n  `--max-tokens N`, `--temperature F`, `--top-p F`, `--top-k N`,\n  `--frequency-penalty F`, `--presence-penalty F`, `--provider-options JSON`.\n- `model large` with no argument prints the current selection as `provider/id`,\n  usable in `$(model large)`.\n\n`large` is the primary coding model; `small` is used for summarization.\n\n### mcp\n\n```bash\nmcp add <name> --type stdio|sse|http [flags]   # default type is stdio\nmcp remove <name>                              # alias: rm\n```\n\nFlags: `--command CMD`, `--args ARG` (repeatable), `--env KEY VALUE`\n(repeatable), `--url URL`, `--header KEY VALUE` (repeatable), `--timeout N`,\n`--disabled BOOL`, `--disabled-tools TOOL` (repeatable), `--enabled-tools TOOL`\n(repeatable), `--oauth BOOL`, `--oauth-client-id ID`, `--oauth-client-secret SECRET`,\n`--oauth-callback-port PORT`.\n\n```bash\nmcp add github --type http \\\n  --url \"https://api.githubcopilot.com/mcp/\" \\\n  --header Authorization \"Bearer $GH_PAT\"\n\nmcp add filesystem --command node --args /path/to/mcp-server.js\n```\n\n### lsp\n\n```bash\nlsp add <name> --command CMD [flags]\nlsp remove <name>                     # alias: rm\n```\n\nFlags: `--args ARG` (repeatable), `--env KEY VALUE` (repeatable),\n`--filetypes TYPE` (repeatable), `--root-markers MARKER` (repeatable),\n`--timeout N`, `--disabled BOOL`, `--init-options JSON`, `--options JSON`.\n\n```bash\nlsp add go --command gopls --env GOPATH \"$HOME/go\"\nlsp add typescript --command typescript-language-server --args --stdio\n```\n\n### hooks\n\n```bash\nhook add <event> --command CMD [--name NAME] [--matcher REGEX] [--timeout N]\nhook remove <event> [--name NAME]    # alias: rm; without --name clears the event\n```\n\nOnly named hooks can be removed individually — give a hook `--name` if you\nintend to remove it later. See [Hooks runtime](#hooks-runtime) for how hooks\nexecute (stdin payload, env vars, decisions).\n\n```bash\nhook add PreToolUse --matcher \"^bash$\" --command \".crush/hooks/no-haskell.sh\" --name no-haskell\n```\n\n### permissions\n\n```bash\npermissions allow <tool> [<tool> ...]   # tools that skip permission prompts\npermissions deny <tool> [<tool> ...]    # hide tools from the agent entirely\n```\n\n`deny` is the inverse of `allow`: it writes `options.disabled_tools`. A denied\ntool is hidden from the agent, not merely prompted for.\n\n### options\n\n```bash\noption <key> [value]\noption reset <list-key>    # clear a list option back to empty\n```\n\n- **Boolean keys** (value optional, defaults `true`): `debug`, `debug-lsp`,\n  `auto-lsp`, `progress`.\n- **Boolean keys phrased positively** (stored as the negated field): `metrics`,\n  `auto-summarize`, `provider-auto-update`,\n  `default-providers`. Example: `option metrics false` disables metrics.\n- **String keys**: `data-directory`, `initialize-as`, `notifications`.\n- **Attribution keys**: `attribution-trailer-style` (`none`, `co-authored-by`,\n  `assisted-by`) and `attribution-generated-with` (boolean).\n- **UI settings**: `option ui compact BOOL`, `option ui diff unified|split`,\n  `option ui transparent BOOL`, `option ui mouse BOOL` (default `true`;\n  disable to let the terminal/tmux handle selection, copy/paste, and\n  scrolling), `option ui scrollbar default|always|never`,\n  `option ui exit-banner default|compact|none`,\n  `option ui completions-max-depth N`, `option ui completions-max-items N`.\n- **List keys** (singular, one value per call, repeatable): `context-path`,\n  `global-context-path`, `skill-path`, `disable-skill`. Use `option reset <key>`\n  to wipe inherited values (e.g. after `source`).\n\n```bash\noption progress false\noption skill-path ./skills\noption disable-skill crush-config\noption attribution-trailer-style assisted-by\noption attribution-generated-with true\noption ui compact true\noption ui diff unified\noption ui exit-banner compact\n```\n\n> [!IMPORTANT] These skill paths are loaded by default and do NOT need\n> `skill-path`: `.agents/skills`, `.crush/skills`, `.claude/skills`,\n> `.cursor/skills`.\n\n## Hooks runtime\n\nHooks are user-defined shell commands that fire on agent events. Currently only\n`PreToolUse` is supported, which runs before a tool executes. This behavior is\nthe same however the hook is defined (`hook add` or JSON).\n\n### How hooks work\n\n1. When a tool is about to be called, all `PreToolUse` hooks with a matching\n   `matcher` (or no matcher) run in parallel.\n2. Duplicate commands are deduplicated — each unique command runs at most once.\n3. The hook receives JSON on **stdin** and hook-specific **environment\n   variables**.\n\nEvent names are case-insensitive and accept snake_case: `PreToolUse`,\n`pretooluse`, `pre_tool_use`, `PRE_TOOL_USE` all work.\n\n### Hook input (stdin)\n\n```json\n{\n  \"event\": \"PreToolUse\",\n  \"session_id\": \"abc-123\",\n  \"cwd\": \"/path/to/project\",\n  \"tool_name\": \"bash\",\n  \"tool_input\": { \"command\": \"ls -la\" }\n}\n```\n\n### Hook environment variables\n\n| Variable                     | Description                                       |\n| ---------------------------- | ------------------------------------------------- |\n| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)                    |\n| `CRUSH_TOOL_NAME`            | Name of the tool being called                     |\n| `CRUSH_SESSION_ID`           | Current session ID                                |\n| `CRUSH_CWD`                  | Current working directory                         |\n| `CRUSH_PROJECT_DIR`          | Project root directory                            |\n| `CRUSH_TOOL_INPUT_COMMAND`   | Value of `command` from tool input (if present)   |\n| `CRUSH_TOOL_INPUT_FILE_PATH` | Value of `file_path` from tool input (if present) |\n\n### Hook output\n\n**Exit code 0** — hook succeeded. Stdout is parsed as JSON:\n\n```json\n{ \"decision\": \"allow\", \"context\": \"optional context appended to tool result\" }\n```\n\n- `decision`: `allow` to explicitly allow, `deny` to block, `none` (or omit).\n- `reason`: explanation (used when denying).\n- `context`: extra context appended to the tool result.\n- `updated_input`: replacement JSON for the tool input; last non-empty wins.\n\n**Exit code 2** — the tool call is blocked; stderr is the deny reason.\n\n**Any other exit code** — non-blocking error; the tool call proceeds.\n\n### Decision aggregation\n\n- **Deny wins over allow** — any deny blocks the call.\n- **Allow wins over none** — a lone allow lets it proceed.\n- Deny reasons and context strings are concatenated (newline-separated).\n- For `updated_input`, the last non-empty value wins.\n\n### Claude Code compatibility\n\nCrush also accepts the Claude Code hook output format, so existing hooks work\nunchanged:\n\n```json\n{\n  \"hookSpecificOutput\": {\n    \"permissionDecision\": \"allow\",\n    \"permissionDecisionReason\": \"Auto-approved\",\n    \"updatedInput\": { \"command\": \"echo rewritten\" }\n  }\n}\n```\n\n## User-invocable skills\n\nSkills can be invoked as commands. Add `user-invocable: true` to the skill's\nYAML frontmatter:\n\n```yaml\n---\nname: my-skill\ndescription: A skill that can be invoked as a command.\nuser-invocable: true\n---\n```\n\n- Global skills appear as `user:skill-name`; project skills as\n  `project:skill-name`.\n- Add `disable-model-invocation: true` to keep a skill user-only (hidden from\n  the model's available-skills list but still manually invocable).\n\n## Environment variables\n\n- `CRUSH_VERSION` — exported into `crushrc` at load; the running version (or\n  `devel` for local builds).\n- `CRUSH_GLOBAL_CONFIG` — override global config location.\n- `CRUSH_GLOBAL_DATA` — override data directory location.\n- `CRUSH_SKILLS_DIR` — override default skills directory.\n\n## Legacy JSON format\n\n`crush.json` is the original static format. It still works and merges with\n`crushrc`. Basic structure:\n\n```json\n{\n  \"$schema\": \"https://charm.land/crush.json\",\n  \"models\": {},\n  \"providers\": {},\n  \"mcp\": {},\n  \"lsp\": {},\n  \"hooks\": {},\n  \"options\": {},\n  \"permissions\": {}\n}\n```\n\nThe `$schema` property enables IDE autocomplete but is optional.\n\n### crushrc ↔ crush.json mapping\n\n| crushrc                             | crush.json                                             |\n| ------------------------------------ | ------------------------------------------------------ |\n| `provider add openai --api-key \"$K\"` | `providers.openai = {\"api_key\": \"$K\"}`                 |\n| `model add openai/gpt-x --name X`    | append to `providers.openai.models[]`                  |\n| `model large openai/gpt-x`           | `models.large = {\"provider\":\"openai\",\"model\":\"gpt-x\"}` |\n| `mcp add gh --type http --url U`     | `mcp.gh = {\"type\":\"http\",\"url\":\"U\"}`                   |\n| `lsp add go --command gopls`         | `lsp.go = {\"command\":\"gopls\"}`                         |\n| `hook add PreToolUse --command C`    | append to `hooks.PreToolUse[]`                         |\n| `permissions allow view ls`          | `permissions.allowed_tools = [\"view\",\"ls\"]`            |\n| `permissions deny bash`              | `options.disabled_tools = [\"bash\"]`                    |\n| `option skill-path ./skills`         | `options.skills_paths = [\"./skills\"]`                  |\n| `option metrics false`               | `options.disable_metrics = true`                       |\n| `option request-timeout 300`          | `options.request_timeout = 300`                        |\n| `option max-background-jobs 200`     | `options.max_background_jobs = 200`                    |\n| `option attribution-trailer-style none` | `options.attribution.trailer_style = \"none\"`        |\n| `option attribution-generated-with false` | `options.attribution.generated_with = false`       |\n\n### Shell expansion in crush.json\n\nIn JSON, only selected string fields are run through the embedded shell at load\ntime (in `crushrc`, everything is native Bash so this table does not apply):\n\n| Surface                                                         | Expansion                          |\n| --------------------------------------------------------------- | ---------------------------------- |\n| Provider `api_key`, `base_url`, `api_endpoint`, `extra_headers` | yes                                |\n| Provider `extra_body`                                           | **no** (JSON passthrough)          |\n| MCP `command`, `args`, `env`, `headers`, `url`                  | yes                                |\n| LSP `command`, `args`, `env`                                    | yes                                |\n| Hook `command`                                                  | runs via `sh -c`, not the resolver |\n\nSupported constructs: `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:+alt}`,\n`${VAR:?message}`, `$(command)`. An unset variable expands to empty; a failing\n`$(command)` is a hard error. A header that resolves to empty is dropped from\nthe request.\n\n### Security note\n\nBoth formats are trusted code. `crushrc` runs entirely, and any `$(...)` in\n`crush.json` runs at load time, with the invoking user's shell privileges,\nbefore the UI appears. Don't launch Crush in a directory whose config you\nhaven't reviewed.\n\n<!-- File: internal/skills/builtin/crush-hooks/SKILL.md (Tokens: ~1841 | Category: anthropic-skill) -->\n---\nname: crush-hooks\ndescription: Use when the user wants to add, write, debug, or configure a Crush hook — gating or blocking tool calls, approving or rewriting tool input before execution, injecting context into tool results, or troubleshooting hook behavior in crush.json.\n---\n\n# Crush Hooks\n\nHooks are user-defined commands in `crush.json` that fire at specific points\nduring execution, giving deterministic control over tool behavior. They run\n**before** permission checks and **only on the top-level agent's** tool calls —\nsub-agent calls (task tool, agentic_fetch, etc.) are not intercepted, though\nthe sub-agent tool call itself is.\n\nFor the full reference, see `docs/hooks/README.md`. This skill covers what you\nneed to author correct hooks.\n\n## Supported Events\n\nOnly `PreToolUse` is currently supported. Event names are case-insensitive and\naccept snake_case (`PreToolUse`, `pretooluse`, `pre_tool_use` all work).\n\n## Configuration\n\n```jsonc\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"^bash$\",              // regex against tool name (optional; omit to match all)\n        \"command\": \"./hooks/my-hook.sh\",   // required: shell command to run\n        \"timeout\": 10                     // optional: seconds, default 30\n      }\n    ]\n  }\n}\n```\n\nProject-level hooks take precedence over global. Matching hooks are deduped by\n`command`, run in parallel, and aggregated in **config order** (not finish order).\n\n## Language\n\n`command` is a shell command, so hooks can be written in any language by\ninvoking the interpreter: `node ./hooks/h.js`, `python3 ./hooks/h.py`,\n`./hooks/h.sh`, inline `echo '…'`, etc. The rest of this skill shows bash, but\nthe input/output contract is identical regardless of language.\n\n## Input\n\n**Environment variables:**\n\n| Variable                     | Description                              |\n| ---------------------------- | ---------------------------------------- |\n| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)           |\n| `CRUSH_TOOL_NAME`            | Tool being called (e.g. `bash`)          |\n| `CRUSH_SESSION_ID`           | Current session ID                       |\n| `CRUSH_CWD`                  | Working directory                        |\n| `CRUSH_PROJECT_DIR`          | Project root directory                   |\n| `CRUSH_TOOL_INPUT_COMMAND`   | For `bash` calls: the shell command      |\n| `CRUSH_TOOL_INPUT_FILE_PATH` | For file tools: the target file path     |\n\n**JSON on stdin:**\n\n```json\n{\n  \"event\": \"PreToolUse\",\n  \"session_id\": \"313909e\",\n  \"cwd\": \"/home/user/project\",\n  \"tool_name\": \"bash\",\n  \"tool_input\": {\"command\": \"rm -rf /\"}\n}\n```\n\n## Output\n\nCommunicate back via exit code (+ stderr) or JSON on stdout.\n\n| Exit Code | Meaning                                                       |\n| --------- | ------------------------------------------------------------- |\n| 0         | Success. Stdout is parsed as the JSON envelope below.         |\n| 2         | Block this tool call. Stderr becomes the deny reason.         |\n| 49        | Halt the whole turn. Stderr becomes the halt reason.          |\n| Other     | Non-blocking error. Logged and ignored; tool call proceeds.   |\n\nExit 2 blocks one tool call (agent sees the reason and can try again); exit 49\nends the whole turn (user takes over). Default to deny — reach for halt only\nwhen letting the agent retry is itself the problem (e.g. secrets detected,\npolicy violation).\n\n**JSON envelope (exit 0):**\n\n```json\n{\n  \"version\": 1,\n  \"decision\": \"allow\",\n  \"halt\": false,\n  \"reason\": \"...\",\n  \"context\": \"Extra info for the model\",\n  \"updated_input\": {\"command\": \"rewritten\"}\n}\n```\n\n- **`decision`**: `\"allow\"`, `\"deny\"`, or omit. `\"allow\"` is **affirmative\n  pre-approval** — it bypasses the permission prompt entirely. Omit it\n  (or `null`) when you only want to inject context or rewrite input without\n  also auto-approving the call.\n- **`halt: true`**: ends the turn (same as exit 49).\n- **`reason`**: shown to the model on deny; to model and user on halt.\n- **`context`**: string **or array of strings**. Appended to what the model\n  sees. Empty entries are dropped.\n- **`updated_input`**: **shallow-merge patch** against `tool_input`, not a\n  replacement. Keys you include overwrite; keys you don't are preserved.\n  Nested objects are replaced wholesale, not deep-merged. Ignored on deny/halt.\n\n## Aggregation (Multiple Hooks)\n\nComposed in **config order**:\n\n- `deny` > `allow` > no opinion. First deny decides; subsequent allows don't override.\n- `halt` is sticky: any hook halting ends the turn.\n- `reason` and `context` concatenate in config order (newline-joined).\n- `updated_input` patches shallow-merge sequentially; later patches win on colliding keys.\n\n## Canonical Examples\n\n### Block destructive commands\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nif echo \"$CRUSH_TOOL_INPUT_COMMAND\" | grep -qE 'rm\\s+-(rf|fr)\\s+/'; then\n  echo \"Refusing to run rm -rf against root\" >&2\n  exit 2\nfi\n```\n\nConfig: `{\"matcher\": \"^bash$\", \"command\": \"./hooks/no-rm-rf.sh\"}`\n\n### Auto-approve read-only tools (inline, no script)\n\n```jsonc\n{\"matcher\": \"^(view|ls|grep|glob)$\", \"command\": \"echo '{\\\"decision\\\":\\\"allow\\\"}'\"}\n```\n\nEvery `view`/`ls`/`grep`/`glob` call now runs without prompting.\n\n### Inject context without auto-approving\n\nEmit only `context` — omit `decision` so the normal permission flow still runs.\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nif [[ \"$CRUSH_TOOL_INPUT_FILE_PATH\" == *.go ]]; then\n  echo '{\"context\": \"Remember: run gofumpt after editing Go files.\"}'\nelse\n  echo '{}'\nfi\n```\n\nConfig: `{\"matcher\": \"^(edit|write|multiedit)$\", \"command\": \"./hooks/go-context.sh\"}`\n\n### Rewrite tool input (shallow merge)\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nread -r input\nrewritten=$(echo \"$input\" | jq -r '.tool_input.command' | some-rewriter)\n\ncat <<EOF\n{\n  \"context\": \"Rewrote command\",\n  \"updated_input\": {\"command\": \"$rewritten\"}\n}\nEOF\n```\n\nIf the original call was `{\"command\": \"npm test\", \"timeout\": 60000}`, the\ntool runs with `{\"command\": \"<rewritten>\", \"timeout\": 60000}` — `timeout` is\npreserved.\n\n## Authoring Checklist\n\n1. Add `#!/usr/bin/env bash` and `set -euo pipefail` (for shell scripts).\n2. `chmod +x` the script.\n3. Add the entry under `hooks.PreToolUse` in `crush.json` with the right matcher.\n4. Decide intent: inject context (omit `decision`), auto-approve (`\"allow\"`),\n   block (`exit 2`), or halt (`exit 49`).\n5. If rewriting input, remember `updated_input` is a shallow merge — only\n   include the keys you want to change.\n\n## Debugging\n\n- Timeouts kill the hook silently and the tool call proceeds. Bump `timeout` if needed.\n- Non-zero exit codes other than 2/49 are logged but don't block — check Crush logs.\n- Use `echo \"debug info\" >&2` for logging without corrupting stdout JSON.\n- `matcher` is a regex against the tool name. Use `^bash$` (not `bash`) if you\n  don't also want to match `mcp_something_bash`.\n\n## Claude Code Compatibility\n\nCrush also accepts Claude Code's `hookSpecificOutput` envelope. One intentional\ndivergence: Crush treats `updated_input` as shallow-merge, Claude Code replaces.\nExisting Claude Code hooks work without modification for the matcher/decision\nparts; revisit any that relied on `updatedInput` fully replacing tool input.\n\n<!-- File: internal/skills/builtin/jq/SKILL.md (Tokens: ~873 | Category: anthropic-skill) -->\n---\nname: jq\ndescription: Use when the user needs to query, filter, reshape, extract, create, or construct JSON data — including API responses, config files, log output, or any structured data — or when helping the user write or debug JSON transformations.\n---\n\n# jq — Built-in JSON Processor\n\nCrush ships a built-in `jq` command (via `github.com/itchyny/gojq`) available\nin the bash tool. No external binary is required.\n\n## Supported Flags\n\n| Flag | Description |\n|------|-------------|\n| `-r`, `--raw-output` | Output strings without quotes |\n| `-j`, `--join-output` | Like `-r` but no trailing newline |\n| `-c`, `--compact-output` | One-line JSON output |\n| `-s`, `--slurp` | Read all inputs into an array |\n| `-n`, `--null-input` | Use `null` as input (ignore stdin) |\n| `-e`, `--exit-status` | Exit 1 if last output is `false` or `null` |\n| `-R`, `--raw-input` | Read each line as a string, not JSON |\n| `--arg name value` | Bind `$name` to a string value |\n| `--argjson name value` | Bind `$name` to a parsed JSON value |\n\nFile arguments after the filter are also supported: `jq '.foo' file.json`.\n\n## Differences from Standard jq\n\nThe built-in uses gojq, which is a pure-Go jq implementation. Key\ndifferences:\n\n- **No object key ordering** — keys are sorted by default; `keys_unsorted`\n  and `-S` are unavailable.\n- **Arbitrary-precision integers** — large integers keep full precision\n  (addition, subtraction, multiplication, modulo, division when divisible).\n- **String indexing** — `\"abcde\"[2]` returns `\"c\"`.\n- **Not supported** — `--ascii-output`, `--seq`, `--stream`,\n  `--stream-errors`, `-f`/`--from-file`, `--slurpfile`, `--rawfile`,\n  `--args`, `--jsonargs`, `input_line_number`, `$__loc__`, some regex\n  features (backreferences, look-around).\n- **YAML** — gojq supports `--yaml-input`/`--yaml-output` but the\n  built-in does not currently expose these flags.\n\n## Common Patterns\n\nExtract a field:\n```sh\necho '{\"name\":\"crush\"}' | jq '.name'\n```\n\nFilter an array:\n```sh\necho '[1,2,3,4,5]' | jq '[.[] | select(. > 3)]'\n```\n\nReshape objects:\n```sh\necho '{\"first\":\"Ada\",\"last\":\"Lovelace\"}' | jq '{full: (.first + \" \" + .last)}'\n```\n\nUse variables:\n```sh\necho '{}' | jq --arg host localhost --argjson port 8080 '{host: $host, port: $port}'\n```\n\nSlurp multiple JSON values:\n```sh\necho '{\"a\":1}{\"b\":2}' | jq -s '.'\n```\n\nCompact output for piping:\n```sh\necho '{\"a\":1}' | jq -c '.a += 1'\n```\n\nRaw string output:\n```sh\necho '[\"one\",\"two\",\"three\"]' | jq -r '.[]'\n```\n\nProcess a file:\n```sh\njq '.dependencies | keys' package.json\n```\n\nNull input for constructing JSON:\n```sh\njq -n --arg msg hello '{\"message\": $msg}'\n```\n\n## Tips\n\n- Pipe jq output to other commands: `jq -r '.url' data.json | xargs curl`\n- Chain filters with `|` inside the expression, not shell pipes.\n- Use `try` to suppress errors on missing keys: `jq 'try .foo.bar'`\n- Use `// \"default\"` for fallback values: `jq '.name // \"unknown\"'`\n- Use `@csv`, `@tsv`, `@base64`, `@html`, `@uri` for format strings.\n</yakaai_skills>"}