Stack: Gum
Gum (charmbracelet/gum) reference: interactive shell prompts, styled output, spinners, logging and layout from shell scripts; every flag, keybinding and exit code verified against gum 2.0.1.
Files4
View on GitHubSKILL.mdskills/clod-stack-gum/SKILL.md
GitHubGum Operative
Reference for
gum, Charm's "tool for glamorous shell scripts". Sources: https://github.com/charmbracelet/gum (README,examples/, command source) and the installed binary. Verified against gum v2.0.1 (released 2026-09-11; module pathcharm.land/gum/v2) on 2026-09-22.
Trigger
Use when: a script calls gum, the user mentions gum or Charm, or a shell script needs a prompt, picker, confirmation, spinner, styled box or structured log line. Also use when Claude itself needs to render styled terminal output from a Bash call.
Role
You are an expert in gum. You know every subcommand, its flags, its keybindings, its stdin/stdout contract and its exit codes. You write gum scripts that behave correctly on the first run, degrade sanely without a TTY and never leak UI chrome into captured output.
1. Mental model
Five facts explain almost everything:
- UI goes to stderr, results go to stdout. Every interactive command draws its TUI on stderr, so
VAR=$(gum choose ...)captures only the answer.gum logalso writes to stderr by default. - Exit codes carry meaning.
0success or "Yes".1"No",esc, nothing selected or a usage error.124timeout.130ctrl+c.gum spinreturns the wrapped command's exit code. - Every flag has an env var. Pattern:
GUM_<COMMAND>_<FLAG>(--cursor.foregroundonchooseisGUM_CHOOSE_CURSOR_FOREGROUND).gum styleis the exception: its vars are unprefixed (FOREGROUND,BORDER,PADDING). Flags override env. - Stdin is an input source for most commands.
choose,filterandtableread options from stdin.inputandwriteread an initial value from stdin.confirmreads a pipedy/yesas an answer and skips the TUI.pager,formatandstyleread content. - Interactive commands need a TTY. Without one they fail with
could not open TTYand exit 1. Claude's Bash tool has no TTY, so only the non-interactive commands (style,join,format,log,table --print,spin,version-check, pipedconfirm,choose --select-if-one) can be run directly.
2. Command map
| Command | Does | Output on stdout |
|---|---|---|
choose |
Pick one or more items from a list | Selected item(s), newline-separated |
confirm |
Yes/No prompt | Nothing (exit code only) unless --show-output |
filter |
Fuzzy-search a list, pick one or more | Selected item(s) |
input |
Single-line text prompt | The text |
write |
Multi-line text prompt | The text |
file |
Pick a file or directory from a tree | Absolute path |
table |
Pick a row from CSV (or --print it) |
Row as CSV; one column with --return-column |
pager |
Scroll through content with search | Nothing |
spin |
Show a spinner while a command runs | The command's output (see below) |
style |
Colour, border, padding, alignment on text | Styled text |
join |
Combine blocks horizontally or vertically | Combined block |
format |
Render markdown, code, template or emoji | Rendered text |
log |
Levelled, structured log lines | Nothing (writes to stderr, or --file) |
version-check |
Assert installed gum matches a semver constraint | Nothing (exit 0/1) |
Full flags, defaults, keybindings and per-command gotchas: commands.md.
3. Exit codes and control keys
| Event | Exit | Where it applies |
|---|---|---|
| Submit / affirmative | 0 |
all |
esc, negative, nothing selected |
1 |
all interactive except table and pager, which exit 0 |
--timeout elapsed |
124 |
all interactive (except confirm, which returns its default) |
ctrl+c |
130 |
all interactive |
| Wrapped command failed | its code | spin |
| Invalid flag value | 1 with a must be one of message |
all |
Universal keys: enter submits, esc quits, ctrl+c aborts. Multi-select toggles differ by command: choose uses space/tab/x, filter uses tab/ctrl+@ (ctrl+space) so space can still be typed into the search. ctrl+a selects all in either when --no-limit is set.
4. Scripting patterns
Capture and branch
type=$(gum choose fix feat docs refactor test chore) || exit 1
scope=$(gum input --placeholder "scope (optional)")
[[ -n $scope ]] && scope="($scope)"
summary=$(gum input --value "$type$scope: " --placeholder "Summary")
body=$(gum write --placeholder "Details (enter submits, ctrl+j for a newline)")
gum confirm "Commit?" && git commit -m "$summary" -m "$body"
The || exit 1 after choose matters: esc and ctrl+c leave the variable empty and a script that carries on will act on nothing.
Multi-select into a zsh array
picked=("${(@f)$(gum choose --no-limit --header "Branches to delete" $(git branch --format='%(refname:short)'))}")
(( ${#picked} )) || exit 1
git branch -D "${picked[@]}"
${(@f)...} splits on newlines only, so items containing spaces survive. In bash use mapfile -t picked < <(gum choose --no-limit ...).
Pick from stdin, act with xargs
brew list | gum choose --no-limit | xargs brew uninstall
git log --oneline | gum filter | cut -d' ' -f1
gh pr list | cut -f1,2 | gum choose | cut -f1 | xargs gh pr checkout
Spinner with clean capture
result=$(gum spin --title "Fetching..." --show-output -- curl -fsS "$url") || {
gum log --level error "Fetch failed" url "$url"
exit 1
}
The spinner draws on stderr, the command's stdout lands in $result and the exit code is the command's own.
Pick or create
name=$(printf '%s\n' $existing | gum filter --no-strict --placeholder "Pick or type a new name")
--strict (the default) only returns a matching item. --no-strict returns the typed filter text when nothing matches, which turns filter into a combo box.
Non-interactive fallback
if [[ -t 0 && -t 2 ]]; then
answer=$(gum choose yes no)
else
answer=${DEFAULT_ANSWER:-no}
fi
Tests for a TTY on stdin and stderr, because that is where gum reads keys and draws. A script that pipes into gum confirm can also answer it: echo y | gum confirm exits 0 without drawing anything.
Version guard
gum version-check '>=2.0.0' 2>/dev/null || { echo "gum >= 2.0.0 required" >&2; exit 1; }
5. Gotchas
writesubmits onenterin v2.ctrl+jinserts a newline andctrl+eopens$EDITOR. The README still saysctrl+d; the source does not bind it.confirmdefaults to Yes.--defaultistrue. Pass--default=falseto start on No. On--timeoutit returns that default; it never exits 124.inputhas a 400-character limit by default. Pass--char-limit 0for unlimited.choosewith both args and stdin treats stdin as--selected, not as extra options. Feed options one way or the other.filtermulti-select output order is unstable (Go map iteration). Usechoose --orderedwhen selection order matters.choose --orderedalso sorts the displayed options alphabetically before tracking selection order.filterwith no args and empty stdin lists files in the current directory, which surprises scripts that expected an error.spinonly shows output on a TTY.--show-outputis a no-op when stdout is not a terminal; the command's output then passes straight through.fileneeds--fileor--directorytrue.--filedefaults to true, sogum file --no-filealone errors; add--directory.log -fargs are strings.%sworks,%drenders as%!d(string=3).-fand-sare mutually exclusive.- Piped stdout strips ANSI.
gum style ... | catshows plain text.CLICOLOR_FORCE=1keeps colour;NO_COLOR=1removes it even on a TTY. - Colours are 0-255 ANSI or
#RRGGBB. Named colours are not accepted. - Every styled element accepts hidden sub-flags. Any
--<prefix>.foregroundflag also takes.background,.bold,.faint,.italic,.underline,.strikethrough,.border,.border-foreground,.border-background,.align,.width,.height,.marginand.padding, even though--helplists only the colours. tablereturns CSV. A selected row with commas inside a field comes back quoted;--return-column N(1-based) avoids parsing it.
Additional resources
- commands.md: every subcommand's flags, defaults, keybindings, stdin/stdout contract and command-specific gotchas
- styling.md: colour values, style flags, hidden sub-flags, env var naming,
formattemplate helpers and themes,joinlayouts - recipes.md: complete worked scripts (conventional commit, branch manager, staged-file picker, table-driven menus, layouts) drawn from
examples/and adapted for zsh