writing-user-outputs
Defines CLI output standards and styling patterns for worktrunk development.
Install
mkdir -p .claude/skills/writing-user-outputs && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/7255" && unzip -o skill.zip -d .claude/skills/writing-user-outputs && rm skill.zipInstalls to .claude/skills/writing-user-outputs
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.Key capabilities
- →Formats stdout for pipeable tools
- →Writes status messages to stderr
- →Handles shell integration via directive files
- →Applies ANSI color nesting rules
- →Flushes output for interactive prompts
How it works
It calls internal wrapper functions that detect terminal capability and manage temporary directive files for shell interaction.
Inputs & outputs
When to use writing-user-outputs
- →Implement error handling messages
- →Format progress bar output
- →Add hint messaging to CLI commands
About this skill
Output System Architecture
Shell Integration
Worktrunk uses split file-based directive passing for shell integration:
- Shell wrapper creates two temp files via
mktemp(cd and exec) - Shell wrapper sets
WORKTRUNK_DIRECTIVE_CD_FILEandWORKTRUNK_DIRECTIVE_EXEC_FILE - wt writes a raw path to the CD file; shell commands to the EXEC file (for
--execute) - Shell wrapper reads the CD file with
cd -- "$(< file)"(no shell parsing) - 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/.
The stderr macros carry the same rule for a different consequence: anstream's
eprint! / eprintln! strip ANSI when stderr isn't a terminal, std's keep it,
so a file importing one but not the other writes escapes on one line of a
message block and not the next under wt … 2>log. eprint! is the half that
slips — it has no newline, so it gets reached for mid-block in a file that
imported only eprintln. Every bare eprint! / eprintln! under src/ must
resolve to anstream's: import it, or qualify the call as
styling::eprintln!(…). check_stderr_macros_come_from_styling in
tests/integration_tests/output_system_guard.rs holds that statically, since
no snapshot can — the suite forces CLICOLOR_FORCE=1, so both printers emit
color and a snapshot agrees whichever macro is in scope. Its
STD_STDERR_ALLOWED_PATHS exempts whole files, not calls, so an entry is only
right where std's macro is right throughout.
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-runpreviews both qualify: a preview is the whole answer when nothing mutates. Human-formatted output belongs here too. Color strips automatically on a pipe (anstream), sowt list | grepstays safe. - stderr → narration about doing it: progress, success/warning/error messages, hints, interactive prompts, and
-v/-vvdiagnostics. - 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 narrates a side effect that already happened → stderr (the edited file is the answer; stdout is empty). wt config shell uninstall --dry-run mutates nothing, so ○ Will remove … @ ~/.zshrc is the only answer there is → stdout. What flips isn't the wording, it's whether a side effect exists to be the answer.
For a split preview, the --format=json payload is the arbiter: a line json would carry goes to stdout, narration json omits stays on stderr. wt step prune --dry-run puts the removal plan on stdout (the same plan json emits) but keeps "Skipped young-branch (younger than 1d)" and "nothing to remove" on stderr. One case ignores all this: a preview shown inside an interactive prompt, such as the ? re-preview during wt config shell install, is mid-prompt narration → stderr.
Examples:
wt list,wt config show→ human table/dump or--format=json, both to stdoutwt step prune --dry-run→ the removal plan to stdout (human or json); "nothing to remove" and skipped-young caveats to stderrwt config shell init→ shell code to stdout (foreval)wt switch→ status messages only (nothing to pipe)
When to page output
Route long, human-oriented stdout through crate::help_pager::show_help_in_pager. The helper TTY-detects internally, so piping (wt … | grep) keeps working.
Page when output is human-oriented (headings, gutters, structure) and plausibly exceeds one screen. Don't page pipe-first data (tables, JSON, shell code), short output, or output already paged by a delegated tool (git diff).
Examples that page: --help, wt config show, wt hook show, wt step {commit,squash} --dry-run. Examples that don't: wt list, wt step diff, wt step eval, --show-prompt (pipe-first by design).
Build the whole output into a String first (don't stream), then:
crate::help_pager::show_help_in_pager(&out, true);
The helper is infallible from the caller's perspective — it falls back to plain stdout itself when no pager is configured, stdout isn't a TTY, or the pager fails.
Security
The split-trust design enforces two trust levels:
WORKTRUNK_DIRECTIVE_CD_FILEholds a raw path (no shell parsing), so it's safe to pass through to alias/hook child processes — a body that writes to it can at worst redirectcd.WORKTRUNK_DIRECTIVE_EXEC_FILEholds arbitrary shell that the wrapper sources verbatim, so wt scrubs this env var from alias/hook child processes. A hook body writing to it would inject shell into the parent session.
All directive env vars are removed from spawned subprocesses by default via
shell_exec::scrub_directive_env_vars(). DirectivePassthrough::inherit_from_env()
re-adds only the CD file for trusted contexts.
Windows Compatibility (Git Bash / MSYS2)
On Windows with Git Bash, mktemp returns POSIX-style paths like /tmp/tmp.xxx.
The native Windows binary (wt.exe) needs a Windows path to write to the
directive file.
No explicit path conversion is needed. MSYS2 automatically converts POSIX
paths in environment variables when spawning native Windows binaries — shell
wrappers can use $directive_file directly. See:
https://www.msys2.org/docs/filesystem-paths/
CLI Output Formatting Standards
User Message Principles
Output messages should acknowledge user-supplied arguments (flags, options, values) by reflecting those choices in the message text.
// User runs: wt switch --create feature --base=main
// GOOD - ackn
---
*Content truncated.*
When not to use it
- →Raw terminal printing without styling
- →Manually managing shell integration state
Limitations
- →Only supports terminal-based environments
- →Strict coupling to the worktrunk library
- →Requires adherence to specific function naming
How it compares
It abstracts shell integration and coloring logic away from command code into a unified output system.
Compared to similar skills
writing-user-outputs side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| writing-user-outputs (this skill) | 1 | 2mo | Review | Intermediate |
| deepwiki-rs | 25 | 9mo | Review | Intermediate |
| rust-learner | 8 | 6mo | Review | Beginner |
| exploring-rust-crates | 3 | 7mo | Review | Beginner |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by max-sixty
View all by max-sixty →You might also like
deepwiki-rs
sopaco
AI-powered Rust documentation generation engine for comprehensive codebase analysis, C4 architecture diagrams, and automated technical documentation. Use when Claude needs to analyze source code, understand software architecture, generate technical specs, or create professional documentation from any programming language.
rust-learner
actionbook
Use when asking about Rust versions or crate info. Keywords: latest version, what's new, changelog, Rust 1.x, Rust release, stable, nightly, crate info, crates.io, lib.rs, docs.rs, API documentation, crate features, dependencies, which crate, what version, Rust edition, edition 2021, edition 2024, cargo add, cargo update, 最新版本, 版本号, 稳定版, 最新, 哪个版本, crate 信息, 文档, 依赖, Rust 版本, 新特性, 有什么特性
exploring-rust-crates
hashintel
Generate Rust documentation to understand crate APIs, structure, and usage. Use when exploring Rust code, understanding crate organization, finding functions/types/traits, or needing context about a Rust package in the HASH workspace.
sync-upstream
rust-lang-cn
Sync Chinese translation repository with upstream rust-lang/nomicon. Use when user wants to check for upstream changes, sync translations, update from upstream, or asks about differences between local translation and upstream English version. Triggers on requests like "sync upstream", "check upstream changes", "update from nomicon", or "sync translation".
code-review
jonatron55
Instructions for reviewing changes and ensuring quality before completion. Use when asking for a review or before committing changes.
release-rust-srec
hua0512
Prepare a new rust-srec application release end to end — pick the next semver version, bump the workspace version, promote the staged unreleased.md notes into versioned en+zh release-notes pages, reset unreleased, refresh the release-notes index/sidebar/GitHub-body, and emit the rust-srec-vX.Y.Z tag