writing-user-outputs
This Claude Code skill documents the CLI output formatting standards and shell integration architecture for the Worktrunk project. Load it before editing any code that produces user-facing output, including calls to message functions like warning_message or error_message, CLI help text, progress UI, or any strings displayed to users.
git clone --depth 1 https://github.com/max-sixty/worktrunk /tmp/writing-user-outputs && cp -r /tmp/writing-user-outputs/.claude/skills/writing-user-outputs ~/.claude/skills/writing-user-outputsSKILL.md
# 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.
```rust
// 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):
```rust
use worktrunk::styling::{eprintln, println, stderr};
// Status messages to stderr
eprintln!("{}", success_message("Created worktree"));
// Primary output to stdout (tables, JSON, pipeable)
println!("{}", table_output);
// Flush before interactive prompts
stderr().flush()?;
```
**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`):
```rust
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:** If this command is piped, what should the receiving program get?
- **stdout** → Data for pipes, scripts, `eval` (tables, JSON, shell code)
- **stderr** → Status for the human watching (progress, success, errors, hints)
- **directive file** → Shell commands executed after wt exits (cd, exec)
Examples:
- `wt list` → table/JSON to stdout (for grep, jq, scripts)
- `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:
```rust
if let Err(e) = crate::help_pager::show_help_in_pager(&out, true) {
log::debug!("Pager failed, falling back to stdout: {}", e);
println!("{}", out);
}
```
## 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 (and legacy compat 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.
```rust
// User runs: wt switch --create feature --base=main
// GOOD - acknowledges the base branch
"Created new worktree for feature from main @ /path/to/worktree"
// BAD - ignores the base argument
"Created new worktree for feature @ /path/to/worktree"
```
**Avoid "you/your" pronouns:** Messages should refer to things directly, not
address the user. Imperatives like "Run", "Use", "Add" are fine — they're
concise CLI idiom.
``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.
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.
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 message generation or command aliases; or troubleshooting wt behavior. Also answers general worktrunk/wt questions.
Create a new worktrunk worktree (optionally in another repo) and switch this session's working directory into it. The branch name is optional — one is picked from the task when omitted. Use when launching a session that should work in its own worktree (e.g. `/wt-switch-create -- <task>`, `/wt-switch-create my-branch -- <task>`, or `/wt-switch-create my-branch ~/workspace/other-repo -- <task>`), or mid-session to move work into a fresh branch.