WR

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.zip

Installs 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.
337 charsno explicit “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Intermediate

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

You give it
Message content and severity level
You get back
ANSI-formatted terminal output

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:

  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/.

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):

FunctionPurpose
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):

FunctionSymbolColor
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 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 stdout
  • wt step prune --dry-run → the removal plan to stdout (human or json); "nothing to remove" and skipped-young caveats to stderr
  • wt config shell init → shell code to stdout (for eval)
  • 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_FILE holds 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 redirect cd.
  • WORKTRUNK_DIRECTIVE_EXEC_FILE holds 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.

SkillInstallsUpdatedSafetyDifficulty
writing-user-outputs (this skill)12moReviewIntermediate
deepwiki-rs259moReviewIntermediate
rust-learner86moReviewBeginner
exploring-rust-crates37moReviewBeginner

Try saying

Example prompts that trigger this skill in your AI assistant.

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.

25170

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 版本, 新特性, 有什么特性

83

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.

33

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".

10

code-review

jonatron55

Instructions for reviewing changes and ensuring quality before completion. Use when asking for a review or before committing changes.

00

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

00

Search skills

Search the agent skills registry