SKILL.md

 1---
 2name: shell-builtins
 3description: 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.
 4---
 5
 6# Shell Builtins
 7
 8Crush's shell (`internal/shell/`) uses `mvdan.cc/sh/v3` for POSIX shell
 9emulation. Commands can be intercepted before they reach the OS by adding
10**builtins** — functions handled in-process.
11
12## How Builtins Work
13
14Builtins live in `builtinHandler()` in `internal/shell/run.go`. This is an
15`interp.ExecHandlerFunc` middleware registered in `standardHandlers()`
16**before** the block handler, so builtins run even for commands that would
17otherwise be blocked. The same handler chain is shared by the stateful
18`Shell` type and the stateless `Run` entrypoint used by the hook runner,
19so builtins are available identically in the `bash` tool and in hooks.
20
21The handler is a switch on `args[0]`. Each case either handles the command
22inline or delegates to a helper function.
23
24## Adding a New Builtin
25
261. **Add the case** to the switch in `builtinHandler()` in `run.go`.
272. **Get I/O from the handler context**, not from `os.Stdin`/`os.Stdout`.
28   This ensures the builtin works with pipes and redirections:
29   ```go
30   case "mycommand":
31       hc := interp.HandlerCtx(ctx)
32       return handleMyCommand(ctx, args, hc.Stdin, hc.Stdout, hc.Stderr)
33   ```
343. **Implement the handler** in its own file (e.g.,
35   `internal/shell/mycommand.go`). The function signature must accept a
36   `context.Context` as the first parameter, plus args, stdin, stdout, and
37   stderr:
38   ```go
39   func handleMyCommand(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
40       // args[0] is the command name ("mycommand"), args[1:] are arguments.
41       // Write output to stdout, errors to stderr.
42       // Return nil on success, or interp.ExitStatus(n) for non-zero exit codes.
43   }
44   ```
454. **Poll `ctx` in every unbounded loop.** Builtins that iterate over
46   input, emit values in a generator-style loop, or do any other work
47   that can exceed a few milliseconds MUST check `ctx.Err()` on each
48   iteration and return it verbatim when non-nil. Hook timeouts rely on
49   this: an unbounded builtin that never polls ctx cannot be interrupted
50   by a hook's `timeout_sec`, and the hook runner will have to abandon
51   the goroutine (see `internal/hooks/runner.go`). Returning `ctx.Err()`
52   (not `interp.ExitStatus(n)`) lets callers distinguish "command exited
53   non-zero" from "we ran out of time".
54   ```go
55   for _, item := range items {
56       if err := ctx.Err(); err != nil {
57           return err
58       }
59       // ... process item
60   }
61   ```
625. **Return values**: return `nil` for success, `interp.ExitStatus(n)` for
63   non-zero exit codes, or `ctx.Err()` on cancellation. Write error
64   messages to `stderr` before returning.
656. **No extra wiring needed** — `builtinHandler()` is already registered
66   in `standardHandlers()`.
67
68## Existing Builtins
69
70| Command | File | Description |
71|---------|------|-------------|
72| `jq` | `jq.go` | JSON processor using `github.com/itchyny/gojq` |