/writing-user-outputs
CLI output formatting standards for worktrunk. Load before editing any code that calls warning_message, hint_message, error_message, info_message, eprintln, or println, or that produces strings the user will see (CLI help, progress UI, snapshot text). Documents ANSI color
$ npx -y skills add max-sixty/worktrunk --skill writing-user-outputs --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/writing-user-outputs
Context preview
The summary Claude sees to decide when to auto-load this skill.
CLI output formatting standards for worktrunk. Load before editing any code that calls warning_message, hint_message, error_message, info_message, eprintln, or println, or that produces strings the user will see (CLI help, progress UI, snapshot text). Documents ANSI color
SKILL.md
writing-user-outputs.SKILL.mdname: writing-user-outputs
description: CLI output formatting standards for worktrunk. Load before editing any code that calls warning_message, hint_message, error_message, info_message, eprintln, or println, or that produces strings the user will see (CLI help, progress UI, snapshot text). Documents ANSI color nesting rules, message patterns, and output system architecture.
metadata:
internal: true
Output System Architecture
Shell Integration
Worktrunk uses split file-based directive passing for shell integration:
1. Shell wrapper creates two temp files via `mktemp` (cd and exec) 2. Shell wrapper sets `WORKTRUNK_DIRECTIVE_CD_FILE` and `WORKTRUNK_DIRECTIVE_EXEC_FILE` 3. wt writes a raw path to the CD file; shell commands to the EXEC file (for `--execute`) 4. Shell wrapper reads the CD file with `cd -- "$(< file)"` (no shell parsing) 5. Shell wrapper sources the EXEC file if non-empty
When neither directive env var is set (direct binary call), commands execute directly and shell integration hints are shown.
Output Functions
The output system handles shell integration automatically. Just call output functions — they do the right thing regardless of whether shell integration is active.
// NEVER DO THIS - don't check mode in command code
if is_shell_integration_active() {
// different behavior
}
// ALWAYS DO THIS - just call output functions
eprintln!("{}", success_message("Created worktree"));
output::change_directory(&path)?; // Writes to directive file if set, else no-op**Printing output:**
Use `eprintln!` and `println!` from `worktrunk::styling` (re-exported from `anstream` for automatic color support and TTY detection):
use worktrunk::styling::{eprintln, println, stderr};
// Status messages to stderr
eprintln!("{}", success_message("Created worktree"));
// Primary output to stdout (tables, shell code, pipeable)
println!("{}", table_output);
// Flush before interactive prompts
stderr().flush()?;Which `println!` is in scope decides whether a closed pipe panics: std's panics on the `BrokenPipe` write error, anstream's drops it. `wt … | head` closes the pipe, so command code imports the `worktrunk::styling` one and no `std::println!` is left in `src/`.
**Output whose ANSI is already decided** declares that once at the top of the command with `worktrunk::styling::ColorChoice::Always.write_global()` and then prints through the same anstream macros — the statusline a shell prompt or Claude Code renders, and the `--help-page` document whose escapes the docs pipeline turns into HTML (`--plain` and `--help-md` declare `Never` the same way). Neither consumer is ever a tty, so without the declaration anstream would strip their color every time — and the test suite would not catch it, because it forces color with `CLICOLOR_FORCE=1`; `test_color_follows_the_consumer` pins the unforced behavior. Declare `Always` only when the pipe is a courier rather than the destination; anything a person reads directly stays on plain anstream, which is what strips color on a pipe and honors `NO_COLOR`.
**`--format=json` answers** go through `crate::output::print_json`, never a hand-rolled `println!("{}", serde_json::to_string_pretty(&v)?)`. It serializes pretty with one trailing newline and prints through anstream, so no `--format=json` surface panics when its consumer stops reading. Before that, thirty call sites had open-coded those two lines, and whether any one of them panicked under `| head -3` came down to which `println!` its module happened to import. `wt switch --format=json` is the one non-caller: it emits its single result as one compact line (still through anstream's `println!`), because that is what a shell loop reads.
**Shell integration functions** (`src/output/global.rs`):
| Function | Purpose | |----------|---------| | `change_directory(path)` | Shell cd after wt exits (writes to directive file if set) | | `execute(command)` | Shell command after wt exits | | `terminate_output()` | Reset ANSI state on stderr | | `is_shell_integration_active()` | Check if directive file set (rarely needed) | | `pre_hook_display_path(path)` | Compute display path for pre-hooks | | `post_hook_display_path(path)` | Compute display path for post-hooks |
**Message formatting functions** (`worktrunk::styling`):
| Function | Symbol | Color | |----------|--------|-------| | `success_message()` | ✓ | green | | `progress_message()` | ◎ | cyan | | `info_message()` | ○ | symbol dim, text plain | | `warning_message()` | ▲ | yellow | | `hint_message()` | ↳ | dim | | `error_message()` | ✗ | red | | `prompt_message()` | ❯ | cyan |
**Section headings** (`worktrunk::styling`):
use worktrunk::styling::format_heading;
// Plain heading
format_heading("BINARIES", None) // => "BINARIES" (cyan)
// Heading with suffix
format_heading("USER CONFIG", Some("@ ~/.config/wt.toml"))
// => "USER CONFIG @ ~/.config/wt.toml" (title cyan, suffix plain)stdout vs stderr
**Decision principle:** stdout carries the command's *answer*; stderr carries *narration* about producing it. The discriminating question is answer-vs-narration, not audience — `wt list` is "for the user" yet belongs on stdout because it *is* the answer. "Is this a message to the user?" doesn't discriminate, because nearly all output is.
- **stdout** → the answer, in whatever format the user selected. Data (tables, JSON, shell code, an expanded template) and `--dry-run` previews both qualify: a preview is the whole answer when nothing mutates. Human-formatted output belongs here too. Color strips automatically on a pipe (anstream), so `wt list | grep` stays safe.
- **stderr** → narration about doing it: progress, success/warning/error messages, hints, interactive prompts, and `-v`/`-vv` diagnostics.
- **directive file** → shell commands executed after wt exits (cd, exec).
The same line can flip streams between modes. `wt config shell uninstall` deletes the file, so `✓ Removed … @ ~/.zshrc` only
Read more
name: writing-user-outputs description: CLI output formatting standards for worktrunk. Load before editing any code that calls warning_message, hint_message, error_message, info_message, eprintln, or println, or that produces strings the user will see (CLI help, progress UI, snapshot text). Documents ANSI color nesting rules, message patterns, and output system architecture. metadata: internal: true
Output System Architecture
Shell Integration
Worktrunk uses split file-based directive passing for shell integration:
1. Shell wrapper creates two temp files via `mktemp` (cd and exec) 2. Shell wrapper sets `WORKTRUNK_DIRECTIVE_CD_FILE` and `WORKTRUNK_DIRECTIVE_EXEC_FILE` 3. wt writes a raw path to the CD file; shell commands to the EXEC file (for `--execute`) 4. Shell wrapper reads the CD file with `cd -- "$(< file)"` (no shell parsing) 5. Shell wrapper sources the EXEC file if non-empty
When neither directive env var is set (direct binary call), commands execute directly and shell integration hints are shown.
Output Functions
The output system handles shell integration automatically. Just call output functions — they do the right thing regardless of whether shell integration is active.
// NEVER DO THIS - don't check mode in command code
if is_shell_integration_active() {
// different behavior
}
// ALWAYS DO THIS - just call output functions
eprintln!("{}", success_message("Created worktree"));
output::change_directory(&path)?; // Writes to directive file if set, else no-op**Printing output:**
Use `eprintln!` and `println!` from `worktrunk::styling` (re-exported from `anstream` for automatic color support and TTY detection):
use worktrunk::styling::{eprintln, println, stderr};
// Status messages to stderr
eprintln!("{}", success_message("Created worktree"));
// Primary output to stdout (tables, shell code, pipeable)
println!("{}", table_output);
// Flush before interactive prompts
stderr().flush()?;Which `println!` is in scope decides whether a closed pipe panics: std's panics on the `BrokenPipe` write error, anstream's drops it. `wt … | head` closes the pipe, so command code imports the `worktrunk::styling` one and no `std::println!` is left in `src/`.
**Output whose ANSI is already decided** declares that once at the top of the command with `worktrunk::styling::ColorChoice::Always.write_global()` and then prints through the same anstream macros — the statusline a shell prompt or Claude Code renders, and the `--help-page` document whose escapes the docs pipeline turns into HTML (`--plain` and `--help-md` declare `Never` the same way). Neither consumer is ever a tty, so without the declaration anstream would strip their color every time — and the test suite would not catch it, because it forces color with `CLICOLOR_FORCE=1`; `test_color_follows_the_consumer` pins the unforced behavior. Declare `Always` only when the pipe is a courier rather than the destination; anything a person reads directly stays on plain anstream, which is what strips color on a pipe and honors `NO_COLOR`.
**`--format=json` answers** go through `crate::output::print_json`, never a hand-rolled `println!("{}", serde_json::to_string_pretty(&v)?)`. It serializes pretty with one trailing newline and prints through anstream, so no `--format=json` surface panics when its consumer stops reading. Before that, thirty call sites had open-coded those two lines, and whether any one of them panicked under `| head -3` came down to which `println!` its module happened to import. `wt switch --format=json` is the one non-caller: it emits its single result as one compact line (still through anstream's `println!`), because that is what a shell loop reads.
**Shell integration functions** (`src/output/global.rs`):
| Function | Purpose | |----------|---------| | `change_directory(path)` | Shell cd after wt exits (writes to directive file if set) | | `execute(command)` | Shell command after wt exits | | `terminate_output()` | Reset ANSI state on stderr | | `is_shell_integration_active()` | Check if directive file set (rarely needed) | | `pre_hook_display_path(path)` | Compute display path for pre-hooks | | `post_hook_display_path(path)` | Compute display path for post-hooks |
**Message formatting functions** (`worktrunk::styling`):
| Function | Symbol | Color | |----------|--------|-------| | `success_message()` | ✓ | green | | `progress_message()` | ◎ | cyan | | `info_message()` | ○ | symbol dim, text plain | | `warning_message()` | ▲ | yellow | | `hint_message()` | ↳ | dim | | `error_message()` | ✗ | red | | `prompt_message()` | ❯ | cyan |
**Section headings** (`worktrunk::styling`):
use worktrunk::styling::format_heading;
// Plain heading
format_heading("BINARIES", None) // => "BINARIES" (cyan)
// Heading with suffix
format_heading("USER CONFIG", Some("@ ~/.config/wt.toml"))
// => "USER CONFIG @ ~/.config/wt.toml" (title cyan, suffix plain)stdout vs stderr
**Decision principle:** stdout carries the command's *answer*; stderr carries *narration* about producing it. The discriminating question is answer-vs-narration, not audience — `wt list` is "for the user" yet belongs on stdout because it *is* the answer. "Is this a message to the user?" doesn't discriminate, because nearly all output is.
- **stdout** → the answer, in whatever format the user selected. Data (tables, JSON, shell code, an expanded template) and `--dry-run` previews both qualify: a preview is the whole answer when nothing mutates. Human-formatted output belongs here too. Color strips automatically on a pipe (anstream), so `wt list | grep` stays safe.
- **stderr** → narration about doing it: progress, success/warning/error messages, hints, interactive prompts, and `-v`/`-vv` diagnostics.
- **directive file** → shell commands executed after wt exits (cd, exec).
The same line can flip streams between modes. `wt config shell uninstall` deletes the file, so `✓ Removed … @ ~/.zshrc` only
Worktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows
Other skills on worktrunk.
- /release
Worktrunk release workflow. Use when user asks to "do a release", "release a new version", "cut a release", or wants to publish a new version to crates.io and GitHub.
Open skill - /running-tend
Worktrunk-specific guidance for tend CI workflows. Adds codecov polling, Rust test commands, labels, and review criteria on top of the generic tend-* skills. Use when operating in CI.
Open skill - /worktrunk
Guidance for Worktrunk (the `wt` CLI) — git worktree management, hooks, and config. Load when editing .config/wt.toml or ~/.config/worktrunk/config.toml; adding, modifying, or debugging hooks (post-merge, post-start, pre-commit, pre-merge, post-switch, etc.); configuring commit
Open skill - /wt-switch-create
Create a new worktrunk worktree (optionally in another repo) and switch this session's working directory into it. Use when launching a session that should work in its own worktree.
Open skill - /worktrunk
Guidance for Worktrunk (the `wt` CLI) — git worktree management, hooks, and config. Load when editing .config/wt.toml or ~/.config/worktrunk/config.toml; adding, modifying, or debugging hooks (post-merge, post-start, pre-commit, pre-merge, post-switch, etc.); configuring commit
Open skill - /wt-switch-create
Create a new worktrunk worktree (optionally in another repo) and switch this session's working directory into it. Use when launching a session that should work in its own worktree.
Open skill

