Crush shell-builtins
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.
install
source · Clone the upstream repo
git clone https://github.com/charmbracelet/crush
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/charmbracelet/crush "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.agents/skills/shell-builtins" ~/.claude/skills/charmbracelet-crush-shell-builtins && rm -rf "$T"
manifest:
.agents/skills/shell-builtins/SKILL.mdsource content
Shell Builtins
Crush's shell (
internal/shell/) uses mvdan.cc/sh/v3 for POSIX shell
emulation. Commands can be intercepted before they reach the OS by adding
builtins — functions handled in-process.
How Builtins Work
Builtins live in
Shell.builtinHandler() in internal/shell/shell.go.
This is an interp.ExecHandlerFunc middleware registered in
execHandlers() before the block handler, so builtins run even for
commands that would otherwise be blocked.
The handler is a switch on
args[0]. Each case either handles the command
inline or delegates to a helper function.
Adding a New Builtin
- Add the case to the switch in
inbuiltinHandler()
.shell.go - Get I/O from the handler context, not from
/os.Stdin
. This ensures the builtin works with pipes and redirections:os.Stdoutcase "mycommand": hc := interp.HandlerCtx(ctx) return handleMyCommand(args, hc.Stdin, hc.Stdout, hc.Stderr) - Implement the handler in its own file (e.g.,
). The function signature should accept args, stdin, stdout, and stderr:internal/shell/mycommand.gofunc handleMyCommand(args []string, stdin io.Reader, stdout, stderr io.Writer) error { // args[0] is the command name ("mycommand"), args[1:] are arguments. // Write output to stdout, errors to stderr. // Return nil on success, or interp.ExitStatus(n) for non-zero exit codes. } - Return values: return
for success,nil
for non-zero exit codes. Write error messages tointerp.ExitStatus(n)
before returning.stderr - No extra wiring needed —
is already registered inbuiltinHandler()
.execHandlers()
Existing Builtins
| Command | File | Description |
|---|---|---|
| | JSON processor using |