Complete Documentation
Trellis, in Plain Terms
Trellis is an AI-native development environment. It runs as a local web app that brings together AI sessions (Claude Code and Codex), terminals, tmux sessions, git worktrees, and logs—both local and remote—into one keyboard-driven interface. The AI sessions get the same access to your services, logs, traces, and crash reports that you have, so debugging starts from real data instead of copy-pasted context.
On Your Machine (Development Environment)
Worktrees as First-Class Environments
Each git worktree represents a separate copy of your code. Trellis treats each worktree as its own environment, with its own terminals, running processes, and logs. Switching branches means switching environments, not re-wiring anything by hand.
Terminal and tmux Management
Trellis creates and manages tmux sessions for you. It defines terminal windows and panes for shells, builds, tests, and diagnostics, so you don’t have to manually manage tmux layouts.
Running Your Application’s Components Locally
Trellis runs and supervises the local instances of the services that make up your application—APIs, workers, daemons, etc.—as part of your development environment. When you rebuild a binary, Trellis detects the change and restarts the affected process automatically.
Local Log Access
You can tail and search the logs produced by those locally running components directly from the Trellis UI.
Reverse Proxy
Trellis can run reverse proxy listeners that mirror your production routing (e.g., Caddy or nginx) locally. Route requests to different backend services by path, with optional Tailscale TLS or custom certificates.
AI Sessions (Claude Code & Codex)
Each worktree can have multiple AI chat sessions — Claude Code, OpenAI Codex, or both side by side. You interact with the agent directly in the Trellis web UI, save transcripts to cases for later reference, and continue previous conversations.
Session Inbox
A small chromeless popup, openable from the inbox icon in any Trellis header, lists every live Claude and Codex session across worktrees. Each row shows a real-time status — running, awaiting your input, stalled on an approval, or errored — alongside a live description of what the agent is doing and how long it’s been there. Click a row and the foreground Trellis window jumps to that session — no scrolling through worktree lists to find the agent that’s waiting on you.
Pair Review & Checklist Runs
Any two live sessions — Claude or Codex, in any worktrees — can be wired into a paired review loop: one session implements, the other reviews, and Trellis relays messages between them until the reviewer approves (a configurable stop signal like LGTM) or a round cap is hit. You watch and steer from a banner on the session pages; typing into either session pauses the loop.
A checklist run builds on pairing to work through a multi-phase plan: the implementer completes one phase, a review pair critiques it until it converges, and the run advances to the next phase — repeating until the implementer signals the checklist is done. Phases that fail to converge pause the run for you to retry, skip, or stop. See Pair Review & Checklist Runs.
Usage & Cost Tracking
Trellis reads the transcript files Claude Code and Codex write locally and turns them into a usage report: cost and tokens per day, per worktree, and per session, priced at API list rates. A badge in every page header shows today’s spend, the Claude chat footer shows the running cost of the current session, and the Usage page has the full breakdown.
Cases
A case is the durable record of a worktree’s effort — one unit of work (bug, feature, investigation, task) that accumulates notes, links, transcripts, evidence, traces, and a commit timeline over its lifetime. Cases are created lazily on the first commit, and at most one is open per worktree.
When the work is done, Wrap Up archives the case directory and bundles it into the final commit, alongside a structured summary that the system generates to make the case searchable later. Intermediate commits along the way are recorded on the case as a per-commit timeline, each with a short Claude-generated description.
Remote Systems (Over SSH)
Remote Terminals
Open terminals on remote machines (staging or production) over SSH directly inside the same web interface you use for local work.
Tailing Remote Logs
Stream logs from remote hosts in real time, without maintaining separate SSH sessions.
Grepping Remote Logs
Search through remote logs—including rotated and compressed logs—using standard Unix tools, but driven from a single UI.
Multi-Host Log Correlation
Search for an ID across multiple remote machines at once, making it practical to follow a request as it moves through several services.
Navigation and Speed
Web UI, Keyboard-First
Trellis runs as a web app (typically on localhost) but is designed to be operated primarily from the keyboard.
Pickers for Everything
A global picker lets you quickly jump between worktrees, terminals, running processes, logs, and workflows.
Keyboard Shortcuts
Common actions—switching context, opening terminals, running commands—are bound to shortcuts so you rarely need to touch the mouse.
Glossary
| Term | Description |
|---|---|
| Service | A long-running process that Trellis manages (starts, stops, restarts). Services are your app’s components—APIs, workers, daemons. In the picker, services appear as #name. |
| Service Log | The stdout/stderr output from a managed service. Stored in an in-memory ring buffer and displayed in the web UI. |
| Log Viewer | A configured source for streaming logs—SSH connections, local files, Docker containers, or custom commands. Unlike service logs, log viewers pull from external sources. In the picker, log viewers appear as ~name. |
| Worktree | A git worktree—a separate checkout of your repository on a different branch. Trellis treats each worktree as an isolated environment with its own services, terminals, and logs. |
| Workflow | A predefined command or sequence of commands you run frequently—builds, tests, deployments. Workflows can have input parameters and run from the UI or CLI. |
| Trace Group | A named collection of log viewers that are searched together during distributed tracing. Allows correlating events across multiple systems by trace ID. |
| Remote Window | An SSH terminal to a remote server, displayed in the same UI as local terminals. In the picker, remote windows appear as !name. |
| AI Session | A Claude Code or Codex chat session, scoped to a worktree. Accessible via the navigation picker with @ prefix and a robot icon. |
| Case | The durable record of a worktree’s effort — one unit of work (bug, feature, investigation, task) accumulating notes, links, transcripts, evidence, traces, a commit timeline, and a generated summary. At most one open per worktree. |
| Picker | The fuzzy-search popup (Cmd+P) for quickly navigating to any terminal, service, log viewer, or page. |
Installing Trellis
Trellis consists of two static binaries with no external dependencies beyond tmux:
- trellis — The main server (web UI, API, service management)
- trellis-ctl — The command-line tool for interacting with the server
Requirements
- tmux (required) - Terminal multiplexer for session management
- Go 1.25+ (required) - Trellis is installed by building from source
Install tmux
Trellis requires tmux for terminal management.
macOS:
brew install tmux
Ubuntu/Debian:
sudo apt install tmux
Fedora/RHEL:
sudo dnf install tmux
Build from Source
There are currently no prebuilt binaries — install Trellis by building it from source. With Go installed, this takes under a minute:
# Clone the repository
git clone https://github.com/wingedpig/trellis.git
cd trellis
# Build both binaries
make build
# This creates:
# trellis - The main server
# trellis-ctl - The CLI tool
Verify Installation
# Check trellis version
./trellis -v
# Check tmux is available
tmux -V
Optional: Install Globally
Move the binaries to a location in your PATH:
sudo mv trellis trellis-ctl /usr/local/bin/
Or add the Trellis directory to your PATH:
export PATH="$PATH:/path/to/trellis"
AI Assistant Integration
Trellis includes a skill file that teaches AI coding assistants (Claude Code, Codex, etc.) how to use trellis-ctl effectively.
Claude Code
Trellis installs the skill automatically: on startup it writes .claude/skills/trellis/SKILL.md into the repo and every worktree (and into new worktrees as they’re created), refreshing it when the bundled version changes after an upgrade. Installed copies carry a managed-by: trellis marker; remove the marker line to take ownership of a copy and stop updates, or disable installation entirely in trellis.hjson:
agent: {
install_skill: false
}
To install manually instead (e.g. with auto-install disabled), copy the skill file to your project’s Claude skills directory:
mkdir -p .claude/skills/trellis
cp /path/to/trellis/SKILL.md .claude/skills/trellis/SKILL.md
Or create a symlink to always use the latest version:
mkdir -p .claude/skills/trellis
ln -s /path/to/trellis/SKILL.md .claude/skills/trellis/SKILL.md
Codex
Codex uses AGENTS.md files placed in your repository. Copy the Trellis skill content to your project root:
cp /path/to/trellis/SKILL.md AGENTS.md
Or append to an existing AGENTS.md:
cat /path/to/trellis/SKILL.md >> AGENTS.md
Codex discovers AGENTS.md files hierarchically from the Git root down to your current directory, concatenating them. See the Codex AGENTS.md documentation for details.
What the Skill Provides
The skill file teaches AI assistants to:
- Check service status and view logs
- Filter logs by time, level, and pattern
- Run workflows and switch worktrees
- Debug crashes using crash reports
- Investigate production errors with distributed tracing
- Send notifications when tasks complete
Initialize a Project
After installation, use trellis init to create a configuration file:
cd your-project
trellis init
This interactive command walks you through:
- Project name
- Server port
- Services to manage
- Build workflow
- Log format
The generated trellis.hjson is fully commented to help you understand and customize all options.
For manual configuration or to see what options are available, see the Configuration Reference.
Troubleshooting
tmux not found
Trellis requires tmux for terminal management. Install it using your package manager:
# macOS
brew install tmux
# Ubuntu/Debian
sudo apt install tmux
# Fedora/RHEL
sudo dnf install tmux
Port already in use
If port 1234 (or your configured port) is in use:
# Find what's using the port
lsof -i :1234
# Use a different port
./trellis -port 8080
Or change the port in your trellis.hjson:
server: {
port: 8080
}
Permission denied for log files
When using file-based log viewers, ensure Trellis has read access to the log files:
# Check permissions
ls -la /var/log/myapp/
# Add user to appropriate group (example for syslog group)
sudo usermod -a -G adm $USER
SSH key prompts for remote logs
Remote log viewers and terminals use SSH. To avoid password prompts:
- Ensure your SSH key is added to ssh-agent:
ssh-add ~/.ssh/id_rsa - Verify you can connect without prompts:
ssh hostname - Check
~/.ssh/configfor proper host configuration
Docker socket access denied
For Docker log viewers, your user needs access to the Docker socket:
# Add user to docker group
sudo usermod -a -G docker $USER
# Log out and back in, then verify
docker ps
kubectl context issues
For Kubernetes log viewers, ensure your context is set correctly:
# List contexts
kubectl config get-contexts
# Switch context
kubectl config use-context my-cluster
# Test access
kubectl get pods -n my-namespace
Services not restarting on binary change
- Verify
watch_binarypath matches the actual binary location - Check the
watch.debouncesetting isn’t too high - Ensure
watching: true(the default) isn’t set tofalse
Next Steps
Continue to Quickstart to configure your first project.
For where Trellis stores its runtime state (crash reports, traces, tmux sessions) and how to reset it, see State and Files.
Quickstart
Get Trellis running with your project in 5 minutes.
1. Create a Configuration File
The easiest way to create a configuration file is with the interactive setup:
cd your-project
trellis init
This walks you through setting up your project with prompts for services, workflows, and log format. The generated file is fully commented.
Alternatively, create trellis.hjson manually in your project root:
{
// Project metadata
project: {
name: "myapp"
}
// HTTP server settings
server: {
port: 1234
host: "127.0.0.1"
}
// Services to run
services: [
{
name: "backend"
command: "./bin/backend"
watch_binary: "./bin/backend"
}
{
name: "frontend"
command: ["npm", "run", "dev"]
work_dir: "./frontend"
}
]
// Workflows (builds, tests, etc.)
workflows: [
{
id: "build"
name: "Build"
command: ["make", "build"]
}
{
id: "test"
name: "Run Tests"
command: ["go", "test", "./..."]
}
]
}
2. Start Trellis
trellis
Trellis will:
- Load your configuration
- Create tmux sessions for each worktree
- Start your services
- Begin watching for binary changes
- Start the HTTP server
3. Open the Web Interface
Open http://localhost:1234 in your browser.
From here you can:
- View service status
- Access terminal windows
- Run workflows
- View logs
Keyboard shortcuts make navigation fast:
Cmd/Ctrl + P- Open navigation picker (search for any terminal, service, or page)Cmd/Ctrl + Backspace- Open history picker (recently visited screens)Cmd/Ctrl + H- Show all shortcuts
4. Use the CLI
From your project directory (or any subdirectory), trellis-ctl finds
trellis.hjson by walking up and connects to the configured host/port.
In Trellis-managed terminals, TRELLIS_API is also set automatically and
takes precedence:
# Check service status
trellis-ctl status
# View logs
trellis-ctl logs backend
# Run a workflow
trellis-ctl workflow run build
# List worktrees
trellis-ctl worktree list
5. Automatic Restarts
When you recompile a binary that a service is watching, Trellis automatically restarts that service. The flow:
- You run
go build -o ./bin/backend ./cmd/backend - Trellis detects
./bin/backendchanged - The
backendservice is gracefully restarted - New logs stream to the UI
6. Open an AI Session
This is where Trellis stops being just a service manager. On startup, Trellis installed a skill file into your repo (.claude/skills/trellis/SKILL.md) that teaches Claude Code how to drive Trellis itself — checking service status, reading logs, running workflows, and tracing errors via trellis-ctl.
Open the navigation picker (Cmd/Ctrl + P), choose your worktree’s home page (@main - Home), and click New Session under Claude Sessions. Then try something like:
Check the backend service logs and summarize any errors from the last hour.
Claude runs trellis-ctl on its own — no copy-pasting logs into a chat window. When it’s done investigating something real, Wrap Up captures the transcript and notes as a case, committed alongside the fix.
The same surfaces exist for OpenAI Codex sessions.
What’s Next
- AI Sessions - Sessions, cost tracking, plans, commits, and wrap-up
- Pair Review & Checklist Runs - Two AI sessions reviewing each other’s work
- Services - Configure service lifecycle and restarts
- Worktrees - Use git worktrees for parallel development
- Configuration Reference - Full config file documentation
Concepts
Services
Services are long-running processes that Trellis manages throughout your development session.
Service Definition
Define services in your trellis.hjson:
{
services: [
{
// Required: unique name
name: "backend"
// Command to run (string or array)
command: "./bin/backend"
// or: command: ["./bin/backend", "-port", "8080"]
// Working directory (default: worktree root)
work_dir: "{{.Worktree.Root}}/services/backend"
// Environment variables
env: {
DB_HOST: "localhost"
DEBUG: "true"
}
// Binary to watch for changes (auto-restart on change)
watch_binary: "{{.Worktree.Binaries}}/backend"
// Additional files to watch (configs, etc.)
watch_files: [
"config/backend.yaml"
"config/database.yaml"
]
// Enable/disable the service (default: enabled)
enabled: true
// disabled: true // Alternative way to disable
// Include in binary watching (default: true)
// Set to false for external services like databases
watching: true
}
]
}
When Services Start
Services start automatically when Trellis launches, unless disabled. Here’s the startup sequence:
- On Trellis launch: All enabled services (
enabled: true, the default) start immediately - On worktree activation: When you switch worktrees, all services stop and restart in the new worktree’s context
- On binary change: When a watched binary changes, only that service restarts
Services with enabled: false or disabled: true won’t start automatically but can be started manually via the UI or trellis-ctl start <name>.
Service Lifecycle
States
| State | Description |
|---|---|
stopped |
Not running |
starting |
Process started |
running |
Running |
stopping |
Graceful shutdown in progress |
crashed |
Exited unexpectedly |
State Transitions
stopped → starting → running → stopping → stopped
↓ ↓
crashed crashed
Binary and File Watching
Trellis can automatically restart services when files change. There are three related settings:
watch_binary
The primary mechanism for auto-restart. When the specified binary file changes, the service restarts:
{
name: "backend"
command: "./bin/backend"
watch_binary: "./bin/backend" // Restart when this file changes
}
watch_files
Additional files to watch beyond the binary. Useful for config files:
{
name: "backend"
command: "./bin/backend"
watch_binary: "./bin/backend"
watch_files: ["config.yaml", "secrets.env"] // Also restart on these
}
Both watch_binary and watch_files trigger restarts independently—a change to any watched file restarts the service.
watching: false
Excludes a service from the binary watching system entirely. Use this for external services (databases, third-party tools) that you don’t build:
{
name: "redis"
command: ["redis-server"]
watching: false // Never auto-restart, even if watch_binary is set
}
When watching: false, the service won’t restart when binaries change, even if watch_binary is configured. The service still starts on Trellis launch and can be controlled manually.
Debounce
File watchers use debouncing to avoid rapid restarts during builds:
{
watch: {
debounce: "100ms" // Wait for rapid changes to settle (default)
}
}
The restart sequence:
- You recompile:
go build -o ./bin/backend ./cmd/backend - Trellis detects the binary changed
- Debounce timer starts (waits for additional changes)
- After debounce period, the service is gracefully stopped (SIGTERM)
- The service restarts
- A
binary.changedevent is emitted
Restart Policies
Control what happens when a service exits:
{
services: [
{
name: "worker"
command: "./bin/worker"
restart_policy: "on-failure" // "always", "on-failure", "never"
max_restarts: 3 // Give up after N attempts
restart_delay: "1s" // Wait between restarts
}
]
}
Alternatively, use a nested restart block with just the policy:
{
services: [
{
name: "worker"
command: "./bin/worker"
restart: {
policy: "on-failure"
}
max_restarts: 3
restart_delay: "1s"
}
]
}
Crash Reports
When a service crashes, Trellis captures:
- Recent log lines (context before the crash)
- Exit code
- Stack trace (if configured)
- Timestamp and worktree context
View crashes with:
trellis-ctl crash newest
trellis-ctl crash list
Service Log Tracing
Services with logging.parser configured (directly or via logging_defaults) are automatically registered as trace-searchable log sources. Trellis creates svc:<name> log viewers backed by each service’s in-memory ring buffer and collects them into a services trace group:
# Search all service log buffers for a trace ID
trellis-ctl trace "req-123" services -since 1h
# View available trace groups
trellis-ctl trace-report -groups
This enables distributed tracing across dev services with zero configuration. Two-pass ID expansion works when the service parser includes an id field (e.g., id: "request_id").
Service Events
Services emit events throughout their lifecycle:
| Event | Description |
|---|---|
service.started |
Service started running |
service.stopped |
Service stopped |
service.crashed |
Service exited unexpectedly |
service.restarted |
Service was restarted |
binary.changed |
Watched binary was modified |
Worktrees
Trellis treats each git worktree as a first-class development environment with isolated services, logs, and terminal sessions.
What are Worktrees?
Git worktrees allow you to have multiple working directories from a single repository. Each worktree can have a different branch checked out:
~/src/myapp/ # main branch
~/src/myapp-feature/ # feature branch (worktree)
~/src/myapp-hotfix/ # hotfix branch (worktree)
Trellis extends this by giving each worktree its own:
- Service instances (binaries from that worktree’s build)
- Terminal sessions
- Log buffers
- Workflow execution context
Worktree Discovery
Trellis automatically discovers worktrees:
{
worktree: {
discovery: {
mode: "git" // Use git worktree list
}
}
}
Discovery runs at startup.
Active Worktree
One worktree is “active” at a time. The active worktree:
- Has its services running
- Is shown by default in the UI
Workflows run in the context of the currently viewed worktree, not necessarily the active one.
Switch worktrees with:
trellis-ctl worktree activate feature-branch
This:
- Stops services in the current worktree
- Switches to the new worktree
- Starts services with binaries from the new worktree
Worktree-Aware Configuration
Use template variables to make your config worktree-aware:
{
services: [
{
name: "api"
command: "{{.Worktree.Root}}/bin/api"
watch_binary: "{{.Worktree.Binaries}}/api"
env: {
CONFIG_PATH: "{{.Worktree.Root}}/config"
}
}
]
}
Available Variables
| Variable | Description |
|---|---|
{{.Worktree.Root}} |
Worktree root directory |
{{.Worktree.Branch}} |
Current branch name |
{{.Worktree.Binaries}} |
Configured binary path |
{{.Worktree.Name}} |
Worktree name (directory name) |
Binary Path Configuration
Configure where binaries are located:
{
worktree: {
binaries: {
path: "{{.Worktree.Root}}/bin"
}
}
}
Terminal Sessions
Each worktree gets its own tmux session. Terminal windows are created on demand from the worktree home page rather than being pre-configured. Windows and Claude sessions can be renamed from the worktree home page using the pencil icon next to each item.
Parallel Development
With worktrees, you can:
-
Work on multiple features simultaneously - Each worktree has its own services running its own binaries
-
Quick bug fixes - Create a worktree from main, fix the bug, deploy, delete the worktree
-
Code review - Check out a PR in a worktree, run the services, test it
-
AI-assisted development - Have Claude working in one worktree while you work in another
Worktree Events
| Event | Description |
|---|---|
worktree.deactivating |
About to switch away from a worktree |
worktree.activated |
Switched to a worktree |
worktree.created |
New worktree discovered |
worktree.deleted |
Worktree removed |
worktree.hook.started |
Lifecycle hook started |
worktree.hook.finished |
Lifecycle hook completed |
Creating Worktrees
From the Web UI
The home page has a Create New Worktree form. Enter a branch name and Trellis will create both the git branch and worktree directory. Optionally check Switch to new worktree to activate it immediately.
New worktrees also get the agent skill file installed at .claude/skills/trellis/SKILL.md automatically (unless disabled via agent.install_skill), so Claude Code sessions in the worktree can drive trellis-ctl right away.
From the Command Line
# Create worktree with new branch
git worktree add ../myapp-feature -b feature-branch
# Create worktree from existing branch
git worktree add ../myapp-hotfix hotfix-branch
Trellis will discover the new worktree automatically.
Removing Worktrees
From the Web UI
Click the Remove button on the home page to delete a worktree. You’ll be asked whether to also delete the associated git branch. Trellis will remove the worktree directory, its binaries directory, and kill the tmux session.
You cannot remove the currently active worktree or the main project worktree.
From the Command Line
# Remove the worktree directory
git worktree remove ../myapp-feature
# Or delete and prune
rm -rf ../myapp-feature
git worktree prune
Logging
Trellis provides unified log viewing across multiple sources with parsing, filtering, and distributed tracing.
Log Sources
Service Logs
Every service automatically captures stdout/stderr in a ring buffer:
trellis-ctl logs backend
trellis-ctl logs backend -f # Follow mode
Log Viewers
For external log sources, configure log viewers:
{
log_viewers: [
{
name: "nginx-logs"
source: {
type: "ssh"
host: "web01.example.com"
path: "/var/log/nginx/access.log"
}
parser: {
type: "json"
timestamp: "time"
level: "status"
message: "request"
}
}
]
}
Source Types
Service Source (Automatic)
Services with logging.parser configured automatically get a log viewer (svc:<name>) that reads from the service’s in-memory ring buffer. These are created at startup and require no manual configuration. See Distributed Tracing for usage.
File Source
source: {
type: "file"
path: "/var/log/app.log"
follow: true
}
SSH Source
source: {
type: "ssh"
host: "server.example.com"
path: "/var/log/app"
current: "current.log"
rotated_pattern: "*.log.*"
}
Command Source
source: {
type: "command"
command: ["journalctl", "-f", "-u", "myapp", "-o", "json"]
}
Docker Source
source: {
type: "docker"
container: "my-container"
follow: true
}
Kubernetes Source
source: {
type: "kubernetes"
namespace: "default"
pod: "my-pod"
container: "app"
follow: true
}
Viewer Modes: Live vs. Explore
Each log viewer opens in one of two modes, set via mode in its log_viewers entry:
log_viewers: [
{
name: "nginx-access"
mode: "explore"
source: {
type: "file"
path: "/var/log/nginx"
current: "access.log"
}
}
]
live(default): opens tailing the source and following new entries — the existing behavior.explore: for high-volume logs (nginx access logs and similar) where tailing every line isn’t useful. Opening the viewer does not start the tail. Instead the server reads a static snapshot of the ~200 most recent lines directly from the end of the file (a byte-offset backward read), and the UI opens paused, with search and scrollback as the primary workflow. A Go live button in the header starts the tail and switches to streaming. Scrolling up to page back through history, and history search, work the same as inlivemode.
explore mode requires a source that supports backward reads — file and ssh. For docker, kubernetes, and command sources (which stream rather than expose a seekable byte offset), an explore-mode viewer falls back to starting the tail immediately but still opens paused, so the UI behaves consistently even though the tail is already running underneath.
Pausing and Auto-Pause
Whichever mode a viewer is in, pausing is lossless and cheap: while a connection is paused (scrolled up, auto-paused, or in explore mode before going live) the server stops shipping individual log lines and instead sends a small stats frame every couple of seconds (missed-line count and current rate). On resume, the server replays the missed lines from an in-memory ring buffer of up to 2000 entries; if more were missed, the newest 2000 are shown with a “N lines skipped while paused” divider, and history search can still locate the rest.
Followed viewers also auto-pause under load: if entries arrive faster than log_viewer_settings.auto_pause_rate (default 30 lines/sec), the UI drops out of following and shows a “High volume — following paused” banner rather than trying to render every line. A viewer that isn’t accessed at all — no active watchers and no polling — is stopped after log_viewer_settings.idle_timeout (default 5m); one whose last watcher just disconnected is stopped sooner, after log_viewer_settings.disconnect_grace (default 30s). See Configuration Reference for the mode, disconnect_grace, and auto_pause_rate settings.
Parsers
JSON Parser
parser: {
type: "json"
timestamp: "ts"
level: "level"
message: "msg"
}
Logfmt Parser
parser: {
type: "logfmt"
timestamp: "time"
level: "level"
message: "msg"
}
Regex Parser
parser: {
type: "regex"
pattern: "^\\[(?P<timestamp>[^\\]]+)\\] (?P<level>\\w+): (?P<message>.*)$"
timestamp_format: "2006-01-02 15:04:05"
}
Filtering
CLI Filtering
# By level
trellis-ctl logs backend -level error
trellis-ctl logs backend -level warn,error
# By time
trellis-ctl logs backend -since 1h
trellis-ctl logs backend -since 6:00am -until 7:00am
# By pattern
trellis-ctl logs backend -grep "connection"
trellis-ctl logs backend -grep "panic|fatal"
# By field
trellis-ctl logs backend -field host=prod1
# Context lines (like grep -B/-A/-C)
trellis-ctl logs backend -grep "error" -B 5 -A 10
Web UI Filter Syntax
Service Log Filters:
| Syntax | Description | Example |
|---|---|---|
field:value |
Field contains value | level:error |
field:~regex |
Regex match on field | msg:~timeout.* |
text |
Full text search | timeout |
Log Viewer Filters:
| Syntax | Description | Example |
|---|---|---|
level:value |
Level contains value | level:error |
-level:value |
Exclude exact level | -level:debug |
msg:~text |
Message contains text | msg:~timeout |
"quoted" |
Message contains text | "error" |
field:value |
Field contains value | host:prod1 |
text |
Full text search | timeout |
Multiple terms are AND-ed together. All matching is case-insensitive.
Trace Report Filters:
Trace reports support all the log viewer filter syntax above, plus:
| Syntax | Description | Example |
|---|---|---|
trace:text |
Show all entries sharing a trace ID with entries matching text |
trace:/api/users |
The trace: prefix performs a three-pass filter: first it finds entries matching the search text, then collects their trace ID values, and finally shows all entries that share any of those trace IDs. This is useful for seeing the full request context across services when you know part of a request (e.g., a URL path or error message).
Distributed Tracing
Search for a trace ID across multiple log sources:
trellis-ctl trace abc123 api-flow -since 1h
Service Tracing (Dev Environment)
When services have logging.parser configured (directly or via logging_defaults), Trellis automatically creates a services trace group that searches all service log buffers:
# Search across all dev service logs
trellis-ctl trace "req-123" services -since 1h
# View available trace groups (includes auto-generated "services" group)
trellis-ctl trace-report -groups
This works with zero configuration — service log viewers (svc:api, svc:worker, etc.) are created automatically from each service’s in-memory ring buffer. Two-pass ID expansion works if the service parser has an id field configured.
Configure Trace Groups
For production log sources, configure trace groups explicitly:
{
trace_groups: [
{
name: "api-flow"
log_viewers: ["nginx-logs", "api-logs", "db-logs"]
}
]
}
View Trace Reports
trellis-ctl trace-report -list
trellis-ctl trace-report debug-session-1
Service Log Parsing
Configure parsing for service logs:
{
services: [
{
name: "api"
command: "./bin/api"
logging: {
parser: {
type: "json"
timestamp: "ts"
level: "level"
message: "msg"
id: "request_id"
file: "source" // Enables "Open in Editor" from log entries
line: "lineno"
}
}
}
]
}
With a parser configured, service logs appear in the table-based log viewer UI with filtering and field display.
Logging Defaults
Set defaults for all services and log viewers:
{
logging_defaults: {
parser: {
type: "json"
timestamp: "ts"
level: "level"
message: "msg"
id: "request_id"
stack: "stack"
file: "source"
line: "lineno"
}
derive: {
short_time: { from: "timestamp", op: "timefmt", args: { format: "15:04:05" } }
}
layout: [
{ field: "short_time", min_width: 8 }
{ field: "level", min_width: 5 }
{ field: "message" }
]
}
}
Individual services and log viewers can override these defaults.
Workflows
Workflows are parameterized commands you can run from the Trellis web UI or CLI. They turn common tasks — builds, deploys, database operations, test runs — into repeatable actions with input dialogs, confirmation prompts, and structured output.
Defining a Workflow
Define workflows in your trellis.hjson:
{
workflows: [
{
id: "build"
name: "Build All"
command: ["make", "build"]
}
]
}
Every workflow needs an id (used in the CLI and URLs), a name (shown in the UI), and either command or commands.
Single vs. Multi-Command
Use command for a single command:
{
id: "test"
name: "Run Tests"
command: ["go", "test", "-json", "-count=1", "./..."]
}
Use commands to run multiple commands sequentially. If any command fails, the remaining commands are skipped:
{
id: "db-reset"
name: "Reset Database"
commands: [
["./bin/dbutil", "reset"]
["./bin/dbutil", "seed"]
]
}
Template Variables
Commands support Go template variables for worktree-aware paths:
{
id: "build"
name: "Build"
command: ["make", "-C", "{{.Worktree.Root}}", "build"]
}
See Template Variables for the full list.
Inputs
Workflows can prompt the user for input before execution. Inputs appear as a dialog in the web UI or as --flag=value arguments in the CLI.
Input Types
text — Free-form text entry:
{
name: "version"
type: "text"
label: "Version Tag"
placeholder: "e.g., v1.2.3"
pattern: "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
required: true
}
select — Dropdown with predefined options:
{
name: "environment"
type: "select"
label: "Target Environment"
options: ["staging", "production"]
default: "staging"
required: true
}
checkbox — Boolean toggle:
{
name: "dry_run"
type: "checkbox"
label: "Dry run (don't actually deploy)"
default: false
}
datepicker — Date selector (defaults to today if no default specified):
{
name: "deploy_date"
type: "datepicker"
label: "Deploy Date"
}
Using Inputs in Commands
Reference input values in commands and confirmation messages with {{ .Inputs.<name> }}:
{
id: "deploy"
name: "Deploy"
inputs: [
{ name: "env", type: "select", options: ["staging", "prod"], required: true }
{ name: "dry_run", type: "checkbox", label: "Dry run", default: false }
]
command: [
"./deploy.sh"
"--env={{ .Inputs.env }}"
"{{ if .Inputs.dry_run }}--dry-run{{ end }}"
]
}
Validation
Text inputs support two validation mechanisms:
| Field | Description |
|---|---|
pattern |
Regex that the value must match |
allowed_values |
Whitelist of acceptable values |
Invalid inputs are rejected before the workflow runs, both in the web UI and CLI.
Confirmation
Require the user to confirm before a workflow runs:
{
id: "db-reset"
name: "Reset Database"
confirm: true
confirm_message: "This will delete all data. Continue?"
commands: [
["./bin/dbutil", "reset"]
["./bin/dbutil", "seed"]
]
}
The confirm_message field supports templates, so you can include input values:
confirm_message: "Deploy to {{ .Inputs.environment }}?"
Output Parsers
The output_parser field controls how Trellis displays workflow output:
| Parser | Description |
|---|---|
go |
Parses Go compiler output. File:line references become clickable links to your editor. |
go_test_json |
Parses go test -json output. Shows pass/fail/skip status per test with timing. |
generic |
Line-by-line output with exit code summary. The default if no parser is specified. |
html |
Renders the output as HTML in the browser. Useful for formatted reports. |
none |
Suppresses output display entirely. |
Example with go_test_json:
{
id: "test"
name: "Run Tests"
command: ["go", "test", "-json", "-count=1", "./..."]
output_parser: "go_test_json"
timeout: "10m"
}
Structured Summary
When an output parser is configured, completed runs also carry a Summary rollup in the status API: error and warning counts, test pass/fail/skip counts, the names of failing tests, and the first error message. This gives programmatic consumers — trellis-ctl -json, the Go client, and AI agents validating their changes — pass/fail detail without re-parsing the raw output. Summary is null for workflows without a parser.
Service Coordination
Workflows can interact with services:
requires_stopped — Stop specific services before the workflow runs, then restart them afterward:
{
id: "migrate"
name: "Run Migrations"
command: ["./bin/migrate", "up"]
requires_stopped: ["api", "worker"]
}
restart_services — Restart all watched services after the workflow completes (useful for build workflows):
{
id: "build"
name: "Build All"
command: ["make", "build"]
restart_services: true
}
Timeouts
Set a maximum duration with the timeout field. If the workflow exceeds the timeout, the process is killed:
{
id: "test"
name: "Run Tests"
command: ["go", "test", "./..."]
timeout: "10m"
}
Values use Go duration syntax: "30s", "5m", "1h".
Running Workflows
Web UI
Open the workflow picker from the navbar or press Cmd/Ctrl + /. Select a workflow to run it. If the workflow has inputs, a dialog prompts you to fill them in. Output is displayed in a dedicated view with the configured parser.
CLI
# List available workflows
trellis-ctl workflow list
# See a workflow's inputs and validation rules
trellis-ctl workflow describe deploy
# Run a workflow (waits for completion)
trellis-ctl workflow run build
# Run with inputs
trellis-ctl workflow run deploy --environment=staging --dry_run=true
# Check status of a running workflow (pass the run ID, not workflow ID)
trellis-ctl workflow status <run-id>
# Cancel a running workflow (pass a run ID, or a workflow ID to cancel
# its most recent in-flight run)
trellis-ctl workflow cancel <id>
Examples
Build Workflow
A simple build that restarts services afterward:
{
id: "build"
name: "Build All"
command: ["make", "build"]
output_parser: "go"
timeout: "10m"
restart_services: true
}
Deploy Workflow
Inputs, confirmation, and service coordination:
{
id: "deploy"
name: "Deploy"
inputs: [
{ name: "environment", type: "select", label: "Environment", options: ["staging", "production"], default: "staging", required: true }
{ name: "deploy_date", type: "datepicker", label: "Deploy Date" }
{ name: "dry_run", type: "checkbox", label: "Dry run", default: false }
]
confirm: true
confirm_message: "Deploy to {{ .Inputs.environment }}?"
command: ["./deploy.sh", "--env={{ .Inputs.environment }}", "--date={{ .Inputs.deploy_date }}", "{{ if .Inputs.dry_run }}--dry-run{{ end }}"]
requires_stopped: ["api", "worker"]
timeout: "15m"
}
Database Query Workflow
Text inputs with HTML output:
{
id: "db-fetch"
name: "DB Fetch"
description: "Fetch a database row by table and ID"
inputs: [
{ name: "table", type: "text", label: "Table", placeholder: "e.g., users", required: true }
{ name: "id", type: "text", label: "Row ID", placeholder: "e.g., 12345", pattern: "^[0-9]+$", required: true }
]
command: ["./bin/dbutil", "fetch", "--table={{ .Inputs.table }}", "--id={{ .Inputs.id }}", "--format=html"]
output_parser: "html"
}
Web Interface
Terminal Page
URL: /terminal
The Terminal page is the main view in Trellis. It provides access to terminals, service logs, log viewers, and remote sessions—all switchable via the navigation picker.
Navigation Picker
Press Cmd+P (or Ctrl+P) to open the navigation picker. Items are prefixed to indicate their type, and each type has a distinct icon in the dropdown:
| Prefix | Icon | Type | Example |
|---|---|---|---|
@ |
terminal | Local terminal | @main - dev |
@ |
robot | Claude session | @main - Session 1 |
@ |
folder-tree | Worktree home | @main - Home |
# |
status dot | Service logs | #api |
~ |
file-lines | Log viewer | ~production-logs |
! |
terminal | Remote window | !admin |
Claude sessions and local terminals both use the @ prefix but are visually distinguished by their icons. You can also access pages, links, and other items through the picker.

History Picker
Press Cmd+Backspace to open the history picker, which shows your recently visited views in order. This lets you quickly toggle between two views.
Links Panel
Press Cmd/Ctrl+K to open a standalone panel listing every configured link on its own — the same terminal.links entries the navigation picker interleaves with terminals and services, but shown together so you can scan and click them without filtering the picker. Click a link to open it; each link reuses a named browser tab, so re-opening the same link focuses its existing tab instead of piling up duplicates.
The shortcut is active only when links are configured (otherwise Cmd/Ctrl+K falls through to the browser). The panel also appears as Open links panel in the Commands & Shortcuts menu (Cmd/Ctrl+H). Define links under terminal.links in your config:
terminal: {
links: [
{ name: "Grafana", url: "https://grafana.example.com" }
{ name: "Docs", url: "https://docs.example.com" }
]
}
Local Terminals (@)
Local terminals connect to tmux windows in your project’s tmux session. Each worktree has its own tmux session.
Configuration: Terminals are created on demand from the worktree home page. Configure tmux settings (history limit, default shell) in terminal.tmux.
Features:
- Full terminal emulation via xterm.js
- Automatic tmux session creation
- Multiple windows per worktree
- Copy/paste support
- Keyboard shortcuts passed through to tmux
Connection: Terminals use WebSocket connections that automatically reconnect if interrupted. Terminals stay connected even when viewing other items—switching back is instant.
Service Logs (#)
Service logs show the stdout/stderr output from your configured services.
Configuration: Each service log corresponds to an entry in the services array. Service logging behavior is controlled by the logging field within each service definition (parser type, field extraction, etc.).
Features:
- Real-time log streaming
- Structured log parsing (JSON, logfmt, etc.)
- Filtering by level, field values, or text patterns
- Entry details panel with field inspection
- Following mode (auto-scroll) or manual scroll
Filtering syntax:
level:error— Filter by log levelmsg:~"timeout"— Filter messages containing “timeout”field:value— Filter by any parsed field- Multiple terms are AND’d together
Connection: Service logs use HTTP polling (1 second interval). Polling stops when you switch to a different view, and resumes when you return.
Log Viewers (~)
Log viewers stream logs from configured sources—local files, remote SSH connections, Docker containers, Kubernetes pods, or custom commands.

Configuration: Each log viewer is defined in the log_viewers array. Configure the source type (file, ssh, command, docker, kubernetes), connection details, and logging options (parser, fields, history settings). The mode field controls whether the viewer opens tailing (live, the default) or paused with a static snapshot for search/scrollback (explore) — see Viewer Modes.
Features:
- Real-time streaming via WebSocket
- Structured log parsing
- Filtering (same syntax as service logs)
- Entry details panel
- Following/paused modes, with automatic pausing on high-volume streams
- History search for past log entries

Live vs. Explore: live viewers (the default) start tailing the source as soon as you open them and follow new entries. explore viewers — meant for high-volume logs like nginx access logs — open paused with the last ~200 lines loaded from the end of the file, so search and scrollback are the primary workflow. Click Go live in the header to start the tail and switch to streaming.
Auto-pause: If a followed viewer streams faster than the configured auto_pause_rate (default 30 lines/sec), it automatically drops out of following and shows a “High volume (≈N lines/s) — following paused” banner. New lines keep being counted while paused; click the +N new lines indicator or the Following button to resume.
Pausing is lossless: whenever a viewer is paused — by scrolling up, by auto-pause, or in explore mode before going live — the server stops sending individual log lines and instead sends a small stats update every couple of seconds (count of missed lines and current rate). Resuming replays the missed lines from the server’s in-memory buffer (up to 2000 entries); if more were missed, the newest 2000 are shown with a “N lines skipped while paused” divider, and history search can still find the rest. Entries otherwise stream in small batches (roughly every 150ms) rather than one message per line, and the view keeps about the last 4000 rows in the browser, trimming the oldest as new ones arrive; scrolling up past what’s rendered reloads older rows from the server’s buffer.
History Search: Click the clock icon to search historical logs. Specify a time range and grep pattern to find past entries.
Connection: Log viewers use WebSocket connections. The connection is closed when you switch to a different view. When you return to the same log viewer, it reconnects and resumes streaming. The underlying tail keeps running briefly after the last viewer disconnects (disconnect_grace, default 30s) so quickly switching back and forth doesn’t restart it; after the grace period with no watchers, the tail is stopped.
Remote Windows (!)
Remote windows provide SSH terminal access to remote servers configured in your trellis.hjson.
Configuration: Each remote window is defined in the terminal.remote_windows array. Specify the name, SSH host, and optional command to run on connection.
Features:
- Full terminal emulation
- SSH connection management
- Same keyboard shortcuts as local terminals
Connection: Remote windows use WebSocket connections with automatic reconnection, similar to local terminals.
Other Picker Items
The navigation picker also includes:
- Output (
@worktree - output) — Workflow execution output - Editor (
@worktree - editor) — Opens VS Code for the worktree - Pages — Links to /worktrees, /status, /events, /crashes, /trace
- Links — Configured external URLs (open in new window/tab)
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
| Cmd/Ctrl+P | Open navigation picker |
| Cmd/Ctrl+Backspace | Open history picker |
| Cmd/Ctrl+K | Open links panel (when links are configured) |
| Cmd/Ctrl+E | Toggle editor (VS Code) |
| Cmd/Ctrl+L | Jump to service log |
| Escape | Close picker / exit current mode |
See Keyboard Shortcuts for the complete list.
AI Sessions (Claude & Codex)
URL: /claude/{worktree}/{session} and /codex/{worktree}/{session}
Trellis provides an integrated chat interface for both Claude Code and OpenAI Codex. Each worktree can have multiple sessions of either agent, allowing you to work with AI assistance in the context of your development environment.
This page describes the Claude interface; nearly everything applies to Codex sessions too — see Codex parity for the short list of differences.

Accessing Claude
- From the worktree home page (
/worktree/{name}) — The Claude Sessions section lists all sessions for that worktree with buttons to create new sessions - From the navigation picker (
Cmd+P) — Claude sessions appear with the@prefix and a robot icon (e.g.,@main - Session 1) - Direct URL —
/claude/{worktree}/{session}opens a specific session
Session Management
Creating Sessions
Click New Session on the worktree home page. You can optionally provide a display name; if left blank, sessions are auto-named (Session 1, Session 2, …).

Renaming Sessions
Click the pencil icon next to a session on the worktree home page to rename it.
Trashing Sessions
Click the trash icon next to a session on the worktree home page. This moves the session to trash — the process is stopped but the session data is preserved.
Forking a Session
While viewing a Claude chat, hover over any completed message and click the branch icon (next to the copy icon) to fork the session at that point.
The fork modal prompts for a name and creates a new session in the same worktree containing everything up to and including the message you clicked. Send your next message in the new session and the conversation resumes from exactly that point. The original session is untouched.
Use this when you want to explore an alternate path from a particular decision point without losing the existing conversation — typical pattern: fork off the last user message, then retry a different approach in the new session.
Moving Sessions to a New Worktree
Click the move icon (arrow leaving a box) next to a session on the worktree home page to move the session — and optionally some of the source worktree’s uncommitted files — into a fresh git worktree.
The move modal:
- Branch name — Supply a branch name for the new worktree. A fresh worktree is created via the same flow as the regular “New Worktree” action (the branch must not already exist;
/in names is converted to-for the worktree directory). - Files — Lists the source worktree’s modified, added, renamed, and untracked files as checkboxes. All are checked by default; uncheck any you want to leave behind. Directories and symlinks are not supported.
- Move — Creates the worktree, moves the selected files into it, reverts them in the source worktree, and rebinds the session. The Claude process restarts in the new directory on your next message.
After completion, you’re redirected to the new worktree’s home page. If any source files could not be reverted, the session move itself still succeeds and the per-file errors are shown.
Show Trash
Click Show Trash on the worktree home page to view trashed sessions. Each trashed session has:
- Restore — Move the session back to the active list
- Permanent Delete — Permanently remove the session and its message history (requires confirmation)
Trashed sessions are automatically purged after 7 days on server startup.
Chat Interface
The Claude page provides a chat interface with:
- Message area — Shows the conversation history with syntax-highlighted code blocks
- Input area — Text input for sending messages to Claude
- Send button — Submit your message
- Cancel button — Stop Claude’s current response (appears while generating)
- ⋮ More actions menu — A drop-up next to the input box collecting the session actions:
- New conversation — Start a fresh conversation within the same session
- View plan — View and edit the session’s captured plan (appears once a plan exists, see Plan Artifacts)
- Save to case — Save the session transcript to a case
- Commit — Make an intermediate commit against the worktree’s open case (see Commit)
- Wrap up — Archive the case and commit in one step (see Wrap Up)
- Pair for review — Wire this session to another for an automated review loop (see Pair Review & Checklist Runs)
- Start checklist run — Drive this session and a reviewer through a multi-phase checklist (see Pair Review & Checklist Runs)
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
| Enter | Send message |
| Shift+Enter | Insert newline without sending |
| Escape | Stop/cancel current response |
Context Usage and Session Cost
The footer shows the session’s accumulated API cost and current context window usage, e.g. $1.23 · 45K / 1M tokens (4%). The context window size is model-aware — 1M tokens for Opus 4.6+/Fable/Sonnet 4.6+, 200K for Haiku and older models — and the readout turns amber at 50% and red at 70%. Hover it for a breakdown of input, cache-read, and cache-write tokens plus the model and session cost.
Cost accumulates across the whole session, including process restarts and --resume, and persists with the session. It is computed by the Claude CLI itself (API list prices — informational if you’re on a subscription plan). Each session’s cost also appears as a badge in the worktree home page session list, and machine-wide totals live on the Usage page.
Model Picker
A model dropdown in the footer (Default, Opus, Sonnet, Haiku, Fable) forces the session onto a model family. Switching is applied live to the running Claude process — no restart, so background tasks and pending permission prompts survive — and takes effect from your next message. The choice persists with the session and is re-applied (via --model) whenever the process respawns.
Two things to know:
- Default means no forced model: the session runs whatever your Claude Code settings (
~/.claude/settings.json→model) resolve to. A fresh session starts on Default — if new sessions keep coming up on a model you don’t expect, that settings default is what’s choosing it. The picker reflects the forced model, or the model actually observed on the session’s responses when no override is set. - After a live switch the model may still introduce itself by the old name if asked — its identity line was written into the system prompt when the process started. The switch is real regardless: every subsequent response is generated (and billed) by the model you picked, which is what the picker and the Usage page report.
Auto-Approve (Skip Permissions)
An auto-approve checkbox in the footer switches the session into Claude Code’s auto mode: tool calls stop raising permission prompts and run immediately, with a background safety classifier vetting each action (and silently declining risky ones). This is deliberately not bypassPermissions — the classifier and the guardrails below stay in force, so it is safer than skipping the checks entirely. The label turns amber while it’s on, and the setting persists with the session.
Toggling is live where the CLI allows it: turning auto-approve off never restarts the process (background tasks survive, and any prompts that were already pending still need answers). Turning it on restarts the process once if it wasn’t started in auto mode — the conversation resumes automatically on your next message.
Pair this with hard guardrails. permissions.deny rules in ~/.claude/settings.json are enforced by the CLI in every mode, including auto, and a denied call is refused silently without prompting. Use them (plus a PreToolUse hook, which also catches indirect invocations like sh -c "ssh …") to make classes of commands — e.g. anything SSH-shaped when your production hosts are one passwordless hop away — impossible regardless of what the agent decides to run:
{
"permissions": {
"deny": ["Bash(ssh)", "Bash(ssh:*)", "Bash(scp:*)", "Bash(sftp:*)", "Bash(tailscale ssh:*)"]
},
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/block-ssh.sh" }] }
]
}
}
The same toggle exists on the Codex page. There it sends approval policy never plus a danger-full-access sandbox on each turn; turning it off restarts the app-server so your configured policies reapply. The Codex guardrail equivalent is an execpolicy rules file — prefix_rule(pattern = ["ssh"], decision = "forbidden", …) in ~/.codex/rules/default.rules — which Codex enforces even when approvals and the sandbox are fully bypassed.
Transcript Import/Export
Importing Transcripts
Click Import Transcript on the worktree home page to load a previously exported transcript JSON file. This creates a new session with the imported conversation history.
Saving to Cases
Click the briefcase button in the Claude chat interface to save the current transcript to a case:
- Select an existing case or create a new one
- Optionally provide a transcript title
- The transcript is saved to the case’s
transcripts/directory
Saved transcripts can be continued from the case detail page using the Continue button, which imports the transcript into a new Claude session.
Plan Artifacts
When Claude works in plan mode and calls ExitPlanMode, Trellis captures the plan as a durable, versioned artifact attached to the session — the plan text comes from the tool call itself, or from the markdown plan file Claude wrote just before presenting it.
- Viewing — A clipboard button appears in the chat toolbar once the session has a plan. It opens a modal showing the latest version rendered as Markdown, with a version badge.
- Editing — Click Edit in the modal to revise the plan. Edits don’t overwrite history; each save appends a new version (marked as user-edited).
- Storage — Plan history persists per session and survives restarts.
- Cases — When a session with a plan is saved to a case (via Save to Case, Commit, or Wrap Up), the latest plan is copied into the case as
plan.md. An existing case plan is never overwritten, so case-level edits stick. See Cases.
Commit (intermediate)
Click the Commit button to make an intermediate commit against the worktree’s open case (creating the case if it’s the worktree’s first commit). The case stays open and the session keeps going — use this to ship shippable pieces of work over the life of the case.
The Commit modal:
- Auto-detects the worktree’s open case. If one exists, it’s shown read-only at the top. If none, the modal shows new-case fields: title (prefilled from a humanized version of the worktree name) and kind (default:
feature). - Lists changed files as checkboxes (all checked by default). Paths inside the live cases directory are rejected — only your selected files are staged.
- Generates a draft commit message using your existing Claude Code setup — no separate API key. The draft describes exactly the files you’ve checked; uncheck a file and hit Regenerate and the new draft covers only what’s left. The draft populates the textarea unless you’ve started typing.
On confirm, Trellis creates the case if needed, snapshots this session’s transcript onto it, refreshes any transcripts already attached, commits your selected files, and records the commit on the case’s timeline. The session stays alive and the case stays open — keep working.
Wrap Up
Click the Wrap Up button when the work is done. Wrap Up runs the same workflow as Commit with one extra flag: the case directory is archived and bundled into the commit, and the session is trashed.
The Wrap Up modal adds (on top of the Commit modal):
- Optional links to attach to the case before archiving.
- Traces to include — saved trace reports from the session.
- Related sessions to archive — sessions from the other agent (Codex if this is Claude) that you want captured into the same case in one shot.
- Component chips — the components touched by the work, derived deterministically from the changed file paths as soon as the modal opens. Click × to prune any before confirming; the surviving set is stored on the case summary and makes archived cases searchable by component.

On confirm, Trellis runs everything Commit does, saves the extras you selected, generates the case’s searchable summary, and archives the case directory into the same commit as your code — so the whole record of the work lands in git in one step. The session is trashed when done, and you’re redirected to the worktree home page.
If anything fails partway through, the archive is rolled back so the case returns to its pre-wrap-up state.
Codex parity
Everything described above also exists on the Codex page (/codex/{worktree}/{session}), including Auto-Approve, with two exceptions: Plan Artifacts rely on Claude Code’s plan mode, and the Model Picker is Claude-only. Codex transcripts saved to a case land in the case’s codex_transcripts/ directory instead of transcripts/.
Under the hood
Mechanics you don’t need day-to-day, collected here for the curious:
- Forking — The Claude CLI’s JSONL resume file is rewritten for the new session, so the process resumes from exactly the fork point on the next message.
- Moving a session — The server creates the new worktree, copies the selected files (preserving mode and relative paths), reverts the source worktree — tracked files via
git checkout --, untracked files via delete — then stops the running Claude process and rebinds the session. Aclaude.session.movedevent is emitted. - Plans — Plan history is stored per session in
.trellis/claude/plans/<session-id>.jsonand exposed atGET/PUT /api/v1/claude/sessions/{session}/plan. - Commit — On confirm the server resolves or creates the case, snapshots the session’s transcript (if the case was just created), refreshes every attached transcript from its live source,
git adds the selected files,git commits, and appends aCommitEntrytocase.jsonwith the SHA, date, message, generated description, and files changed. The draft message comes fromclaude -p; inputs include the diff of your checked files (the staging area is not consulted), the case manifest,notes.md, and the last few user messages from the session. - Wrap Up — Runs the Commit steps, merges new links, saves selected traces, captures selected related sessions (transcript saved, session trashed), generates the case summary via
claude -psynchronously (with a timeout) so it lands in the same commit, moves the case directory fromcases/tocases-archived/,git adds the selected files plus the archived directory, commits, and trashes the active session. A failure between archive and commit rolls the archive back. - Codex — The wrap-up modal (
static/js/wrapup.js) and the server-sidecommitToCaseorchestrator are agent-agnostic; the only per-agent differences are the Save-to-Case button label and the transcript directory.
Related
- Pair Review & Checklist Runs — Automated implementer/reviewer loops between two sessions
- Cases — Case lifecycle, commits timeline, generated summaries
- Terminal Page — Claude sessions in the navigation picker
Pair Review & Checklist Runs
Trellis can wire two AI sessions together so they review each other’s work without you relaying messages by hand. There are two levels:
- Paired review loop — one implementer session and one reviewer session iterate on a single piece of work until the reviewer approves it.
- Checklist run — an outer loop that drives a pair through a multi-phase plan: the implementer completes one phase, a paired review loop reviews it, and the run advances to the next phase until the whole checklist is done.
Both are ad hoc: you create them on the fly between any two live sessions — Claude or Codex, in any combination, in the same or different worktrees. No configuration file changes are needed.
Paired Review Loop
What it does
A pair has two fixed roles:
- Implementer — produces or revises the work, and reacts to reviewer feedback.
- Reviewer — critiques each iteration and signals convergence by replying with a stop signal (default
LGTM) on a line of its own.
Once running, the loop watches the implementer session. When it finishes a turn and goes idle, the loop captures its last assistant message, prefixes it with your review prompt, and sends it to the reviewer. The reviewer’s critique is prefixed with your feedback prompt and relayed back. This repeats until the reviewer emits the stop signal, the round cap is hit, or you intervene.
Starting a pair
On a Claude or Codex session page, open the ⋮ More actions menu next to the input box and choose Pair for review. The modal asks for:
| Field | Default | Meaning |
|---|---|---|
| Role / Swap | This session = Implementer | Which side the current session plays. Swap flips the roles. |
| Partner session | — | Any other live session (Claude or Codex, any worktree). |
| Review prompt | Review this. If it is good, reply with LGTM on its own line. |
Prefixed to each implementer→reviewer relay. |
| Feedback prompt | Feedback: |
Prefixed to each reviewer→implementer relay. May be empty. |
| Stop signal | LGTM |
Ends the loop when the reviewer puts it on a line by itself (case-insensitive). LGTM with one nit: does not match — context around a signal-only line is fine. |
| Max rounds | 10 | Cap on relays before the loop stops unconverged. |
| Kickoff | Wait for implementer’s next turn | Or Use implementer’s current last message to relay the existing last message immediately as round 1. |
| Confirm before each relay | off | Review and edit every outbound message before it is sent (see below). |
Your settings are remembered as defaults for the next pair.
The pair banner
While a session is in an active pair, an amber banner appears at the top of its page: partner, your role, round count, and the current step (waiting for a side, relaying, paused).

Controls:
- Pause / Resume — suspend and resume the loop.
- Stop — end the loop (the record is kept for audit).
- Force relay — capture and relay the current message immediately instead of waiting for idle detection.
- Settings — edit prompts, stop signal, round cap, and confirm-mode mid-loop; changes apply on the next relay. Partner and roles are fixed.
- Review pending relay… — appears in confirm mode when a relay is waiting for you; opens an editor where you can edit, send, or skip the message, or stop the loop.
When a relay lands, the browser follows the action: if the receiving session is a different one, Trellis navigates to it so you’re always watching the side that is generating.
Staying in control
- Typing into a paired session pauses the loop. If you send a message directly to either side mid-loop, the pair auto-pauses so your conversation and the loop don’t interleave. Resume it from the banner when you’re done.
- A pair stops automatically if a participant session errors out or is trashed.
- Active pairs survive Trellis restarts — state is persisted after every step and rehydrated on startup.
Checklist Runs
What it does
A checklist run automates working through a phased plan — for example a CHECKLIST.md or a spec with ## Phase N sections. The run is checklist-agnostic: the implementer owns and tracks the checklist file; Trellis never parses it. The run just pumps the loop:
- The implementer implements one phase.
- An inner paired review loop reviews that phase until the reviewer approves it (
LGTM). - The run sends the advance prompt (“implement the next phase…”) and the cycle repeats.
- When no phases remain, the implementer replies with the completion signal (default
COMPLETED) alone on a line, and the run stops as completed.
Both sessions keep their full context for the whole run — the implementer carries knowledge between phases, and the reviewer can catch a later phase breaking an earlier one.
Starting a run
Ask the implementer session to work from a checklist file (or write one first — a numbered/phased plan checked into the worktree works well). Then open the ⋮ More actions menu and choose Start checklist run. The modal collects:
| Field | Default | Meaning |
|---|---|---|
| Role / Swap, Partner | This session = Implementer | Same as pairing. |
| Advance prompt | Implement the next phase from the checklist… |
Sent to the implementer to start each phase after the first. Must tell it to reply with the completion signal when nothing is left. |
| Completion signal | COMPLETED |
The implementer’s “no phases left” sentinel — matched only as a reply that is exactly this one line. |
| Review prompt | Review the implementer's work on the current phase… |
Used by each phase’s review pair. |
| Feedback prompt | Feedback: |
Used by each phase’s review pair. |
| Review stop signal | LGTM |
The reviewer’s approval word — distinct from the completion signal. |
| Max rounds per phase | 10 | If a phase’s review hits this cap without approval, the run pauses for you. |
If you change the completion signal or review stop signal, update the corresponding prompt to name the new word — the prompts are sent verbatim.
Starting a run does not prompt the implementer. The implementer’s current (or next) turn is treated as the first phase, so you kick off phase 1 yourself — typically by sending “implement the first phase of docs/my-checklist.md” — and the run takes over from there.
The run banner
Sessions in a run show a blue banner: partner, your role, the current phase number, its status (implementing, under review, starting next phase), and how many phases are done. Controls:
- Pause / Resume — suspend the run (pausing mid-review also pauses the inner pair).
- Skip phase — abandon the current phase and advance.
- Retry phase — re-run review on the implementer’s current output with a fresh round counter (offered while paused).
- Stop — end the run.
During a phase’s review the checklist banner reports the state; the inner pair’s banner only appears when it needs your attention (paused, or waiting on a relay confirmation).
When a phase doesn’t converge
If a phase’s review hits the round cap without an approval, the run pauses instead of advancing — it never moves past unreviewed work. From the paused banner you choose: Retry, Skip, or Stop. Every attempt is recorded in the run’s phase history for audit.
Runs persist to disk after every step and are rehydrated on server restart: a running phase re-attaches to its review pair, and a paused run stays paused until you resume it.
API
Pairing lives under /api/v1/pair and checklist runs under /api/v1/checklist — creation, listing, per-id control actions (pause, resume, stop, and for runs skip / retry), and read-only WebSockets for live updates. The full specifications, including the persisted record formats, are in PAIRING_SPEC.md and PHASE_LOOP_SPEC.md in the repository.
Related
- Claude Page — the session interface both features build on
- Session Inbox — watch every session’s live state while a loop runs
Session Inbox
URL: /inbox (opened as a small popup window, not a regular tab)
The session inbox is a chromeless floating window that lists every active Claude and Codex session across every worktree, with a live state badge. Click a row and the foreground Trellis window jumps to that session — without leaving whatever screen you were on to scroll through worktrees.

Opening the inbox
Click the inbox icon in the top-right of any Trellis page header, or press Cmd/Ctrl + I. The first invocation opens the popup; subsequent invocations reuse the same window.
The popup is sized to roughly 420×720 — small enough to dock alongside an editor or browser, large enough to hold a useful list.
What it shows
Two stacked sections:
- Needs you — sessions waiting for something: a permission prompt (Claude), an approval request (Codex), a turn that finished and is awaiting your next message, or a turn that ended in an error.
- Running — sessions actively generating.
Each row carries:
- A status indicator that reflects the row’s reason:
- a blue dot while running,
- a yellow dot while awaiting your input,
- a pulsing red hand when the agent is stalled on a permission/approval prompt,
- a red warning triangle when the last turn errored.
- The session’s display name.
- A second line showing the worktree and — while running — a live activity description of what the agent is doing right now (
Running go test,Editing schema.go,Thinking…). It updates in place at each tool/step boundary. - A time-in-state label (
4m,2h) showing how long the session has sat in its current state. - A small
CLAUDEorCODEXagent badge. - A hide button (eye-with-slash) on hover.
Within Running, newer state transitions float to the top. Within Needs you, the most urgent reason comes first — stalled approvals, then errors, then turns merely awaiting input — with ties broken by most-recent transition.
Clicking a row
Clicking a row makes your main Trellis window — not the popup — jump to that session. The popup stays where it is, so you can keep triaging. If you have no Trellis tab open at all, a new one is opened for you.
The jump is recorded in navigation history, so Cmd+Backspace (or your configured back binding) takes you back the same way it does after any other navigation.
Hide button
The eye-with-slash next to a row hides that row until its state changes next. Useful when you have a long-running build agent sitting in “running” that you don’t want cluttering the view. As soon as the agent transitions (finishes, errors, asks for input), it pops back into the list. Hides survive closing and reopening the popup.
Live updates
Rows update in real time: state badges flip the moment a session starts or stops needing you, and the activity line refreshes at each tool/step boundary without reordering the list. The footer shows live while the connection is up and offline (reconnecting…) while it’s not — the popup reconnects automatically. Time-in-state labels tick over client-side every 30 seconds with no extra server traffic.
Under the hood
Mechanics you don’t need day-to-day:
- State model —
stateis a coarse two-value field derived per agent: Claude isrunningwhile generating with no pending control request, Codex while generating with no pending approvals; otherwiseneeds_you.statedrives sorting and transition detection — it flips only on a realrunning ↔ needs_youchange. A finerreasonfield (running,awaiting_input,needs_approval,error) refines presentation only and never reorders the list. The aggregator merges both agents’ session lists and timestamps each session’s most recent transition so the UI can sort by recency. - Event streams — the popup subscribes to
session.state_changed(fired only on coarserunning ↔ needs_youtransitions, carrying{session_id, agent, worktree, display_name, state, reason, unread, trashed}) andsession.activity(a lighter stream carrying{session_id, activity}, fired at tool/step boundaries — not per token — and only when the description changes). - Navigation —
GET /api/v1/inbox/ws?role=inbox|mainis a single WebSocket endpoint serving two roles: the popup connects asrole=inbox; every regular Trellis page connects asrole=main(viainbox_main_ws.jsin the shared header). A row click sends{type:"navigate", path:"..."}, which the server forwards to every main-window connection. If no main window is connected, the server replies{type:"navigate_failed", reason:"no_main_window"}and the popup opens a newtrellis-mainwindow directly. - Initial list —
GET /api/v1/inbox/sessionsreturns the merged list ofSessionRow{id, agent, worktree, display_name, state, reason, activity, unread, last_state_change_at}entries. - Hides — stored in
localStoragewith the state they were hidden in; a hide is discarded as soon as the recorded state no longer matches the live one.
Related
- Claude Page — what the inbox row points at for Claude sessions
- Cases — sessions get exported into cases at wrap-up time
Cases
A case is the durable record of a worktree’s effort — one logical unit of work that accumulates notes, transcripts, evidence, traces, a commit timeline, and a generated summary over its lifetime. When the work is done, the case is wrapped up: the directory is archived and the final state is committed to git alongside the code.
Cases are created lazily — usually on the first commit a worktree makes. Worktrees that never commit (throwaway experiments, abandoned work) never get a case.
Lifecycle invariants
- One open case per worktree. Creating a second open case is refused. If you have a second unrelated effort, make a new worktree.
- All commits in a worktree bind to its open case. Both Claude and Codex sessions in the same worktree write their commits to the same case without prompting.
- The case ID is immutable. The title is freely editable; the
iddirectory name and any[case: <id>]references in commit messages are permanent. - Wrap Up is Commit + Archive. Same workflow, with a flag that tells the server to also move the case directory into the archived path and bundle it into the commit.
Worktree home — Cases section
The worktree home page (/worktree/{name}) shows the worktree’s single open case (if any) and:
- Archived cases — Link to the per-worktree archived-cases browser.
- New Case — Only available when no open case exists.
- Archive button on the case row — Moves the case to the archived directory without committing.
Archived cases page
URL: /worktree/{name}/archived-cases
A per-worktree browser for finished work, designed to be a usable memory system for half-remembered cases.
- Full-text search across title, summary fields (synopsis, symptoms, root cause, resolution), components, commit descriptions, and
notes.md. Component matches rank highest. - Filters: kind (bug/feature/investigation/task), date range, “has linked traces” toggle.
- Optional transcript scan — A separate checkbox extends the search into attached transcript previews. Slow path; off by default.
- Sort: date (default), kind, duration, worktree of origin.
Results show the synopsis, component chips from the generated summary, and the first matching snippet for context.
Case detail page
URL: /case/{worktree}/{id}

Header
- Title with an inline edit button. The ID is shown as an immutable monospace tag.
- Kind and status badges, created/updated timestamps.
- Action bar: Back, Wrap Up (non-archived), Archive / Reopen, Delete.
Summary (generated)
When a case has been wrapped up, the page shows the structured summary written by the claude -p generator at wrap-up time:
| Field | Use |
|---|---|
| Synopsis | One human-readable line. |
| Symptoms | The observable problem (empty for non-bug work). |
| Root cause | What was actually wrong (empty for features / wontfix). |
| Resolution | What changed — approach, not the diff. |
| Components | Service / package / subsystem names touched. |
Each field is individually editable. The Regenerate Summary button re-runs generation, confirming first if the existing summary may have been hand-edited.
Commits
A reverse-chronological timeline of the intermediate commits made against the case during its active life. Each row shows:
- Short SHA
- Date
- First line of the git commit message
- The per-commit case description (the “narrative beat”, distinct from the commit message)
The wrap-up commit is intentionally not in this list — it is locatable from git history.
Links
External references (URLs). Add inline with title + URL; remove with the × button.
Plan
Markdown content from plan.md, rendered in the browser. Edit in-place with the pencil button. The plan is seeded automatically from the session’s captured plan artifact when a Claude session is saved to the case; once present it is owned by the case and never overwritten by later transcript saves. Shown read-only on archived cases that have one.
Notes
Markdown content from notes.md, rendered in the browser. Edit in-place with the pencil button.
Evidence
Attached files with format badges and tags.
Transcripts
Saved Claude and Codex transcripts. Each row shows:
- Title and message count
- Whether the live source session has more recent messages (and an Update button to refresh the stored copy)
- Continue — Imports the transcript into a new session for continued work
Traces
Linked trace reports — each linked to a read-only viewer within the case. Saved as full report data, so they remain viewable even if the original report is later deleted.
Commit and Wrap Up
The Commit and Wrap Up buttons on the Claude and Codex session pages share a single modal and a single server-side orchestrator.
Commit (intermediate)
Click Commit on the Claude or Codex session page to make an intermediate commit against the worktree’s open case (creating the case if it’s the worktree’s first commit).
The modal:
- Auto-detects the worktree’s open case. If none, shows a small title + kind form prefilled from a humanized version of the worktree name.
- Lists changed files as checkboxes (all checked by default).
- Generates a draft commit message when it opens. The draft populates the textarea unless you’ve started typing; a Regenerate button is always available. A short per-commit case description is generated alongside it and stored on the resulting commit entry.
On confirm, Trellis creates the case if needed, snapshots the session’s transcript onto it, refreshes any transcripts already attached, commits your selected files, and records the commit on the case’s timeline. The session stays alive and the case stays open — keep working.
Wrap Up
Click Wrap Up when the work is done. It runs everything Commit does, then: merges any new links, saves selected traces, captures selected related sessions from the other agent, generates the case’s searchable summary, and archives the case directory into the same commit as your code — the whole record of the work lands in git in one step. The active session is trashed when done.
If anything fails partway through, the archive is rolled back so the case returns to its pre-wrap-up state.
The exact step-by-step mechanics are documented in AI Sessions: Under the hood.
Generated commit messages and summaries
Generation uses your existing Claude Code authentication — there is no separate API key to configure. Trellis uses claude -p for two distinct purposes:
- Commit message + per-commit description when the Commit / Wrap Up modal opens. The diff fed to the model is built from exactly the files you’ve checked in the modal — uncheck a file and Regenerate, and the new draft describes only what’s left selected. The staging area is not consulted at all. Other inputs: the case manifest,
notes.md, and the last few user messages from the session. - Case summary at wrap-up. The wrap-up diff is scoped the same way (your selected files only). Other inputs: attached transcripts,
notes.md, the per-commit descriptions accumulated during the case, linked trace summaries, and the casekind/status.
Failures degrade gracefully: an empty textarea (you type the message), or a missing summary{} block that you can regenerate from the case detail page.
File structure
Cases are stored as directories under the configured cases.dir (default: trellis/cases):
trellis/cases/
20260514__ach-payments-stripe/
case.json # Manifest including commits[] and summary{}
notes.md # Human narrative
plan.md # Plan artifact (seeded from the session's captured plan)
evidence/ # Attached files
transcripts/ # Saved Claude transcripts
codex_transcripts/ # Saved Codex transcripts
traces/ # Saved trace reports
At wrap-up, the directory moves to trellis/cases-archived/ and is included in the same commit as the user-selected files.
Related
- Configuration: cases — Cases configuration
- Claude Page — Where Commit and Wrap Up are initiated from
Usage Page
URL: /usage
The Usage page shows token usage and cost for your AI coding agents — both Claude Code and OpenAI Codex — computed from the transcript files the agent CLIs write locally. No API keys or network calls are involved; Trellis reads the same on-disk data that tools like ccusage use.
Open it from the navigation picker (Cmd+P, type /Usage), the command palette (Shift+Cmd+P, “View: Token usage & costs”), or by clicking the cost badge in the header.

Data Sources
| Agent | Source | Location |
|---|---|---|
| Claude Code | Transcript JSONL files | ~/.claude/projects/ and ~/.config/claude/projects/ (honors CLAUDE_CONFIG_DIR) |
| Codex CLI | Rollout JSONL files | ~/.codex/sessions/ (honors CODEX_HOME) |
Costs are computed from token counts at current API list prices, including cache-read and cache-write rates. If you’re on a subscription plan (Claude Max, ChatGPT Pro), the dollar figures are what the usage would have cost at API prices — useful for judging how much value you’re getting from the subscription.
Files are parsed once and cached by modification time, so refreshes are fast even with months of history.
Summary Cards
- Today — Total cost and tokens across all projects on this machine, with a Claude/Codex split when both have usage
- Last N days — Period total (7, 30, or 90 days, selectable)
- Daily average — Average cost over days that had usage
- API calls — Total number of model calls in the period
Daily Usage
One row per calendar day across all Claude Code and Codex usage on the machine (not just this project): models used, input/output tokens, cache read/write tokens, and cost. Model badges (opus-4-8, gpt-5.5, …) show which models drove the spend.
By Worktree
Usage attributed to this project’s worktrees, matched by the working directory recorded in each transcript. Worktree names link to the worktree home page.
Top Sessions by Cost
The most expensive agent sessions in this project for the period, with an agent badge (claude/codex), the models used, and last activity time. Capped at the top 50 by cost.
Header Cost Badge
Every Trellis page shows today’s total agent spend in the header (e.g. $12.40 today), refreshed every 5 minutes. Click it to open the Usage page. The badge is hidden when there’s no spend yet today.
Per-Session Cost
Live cost also appears outside this page:
- Claude chat footer — The session’s accumulated cost displays next to the context usage (e.g.
$1.23 · 45K / 1M tokens (4%)); hover for a token and cost breakdown - Worktree home page — Each Claude session row shows a cost badge
Retention
Claude Code prunes transcripts after about 30 days by default, which bounds the history this page can show. To keep more, raise cleanupPeriodDays in your Claude Code settings.json.
Worktrees Page
URL: / (also available at /worktrees)
The Worktrees page is the home page. It lets you manage git worktrees for your project. Each worktree provides an isolated working directory with its own branch, allowing you to work on multiple features or fixes simultaneously.

Page Header
The page header shows the Trellis logo, a brief description of the project, the version number, and links to:
- Documentation — Opens the Trellis documentation site
- GitHub — Opens the Trellis GitHub repository
- Email Group — Opens the Trellis mailing list on Groups.io
Worktree List
The page displays all worktrees with the following information:
- Name — The worktree directory name (derived from branch name)
- Branch — The git branch checked out in that worktree
- Status indicators:
- dirty — The worktree has uncommitted changes to tracked files (untracked files are ignored)
- ↑N / ↓N — Commits ahead or behind the default branch (main/master)
- Detached — The worktree is in detached HEAD state
- Current — Indicates which worktree Trellis is currently using
Status indicators (dirty, ahead/behind) load asynchronously after the page renders. The page displays immediately using cached worktree data, then fetches fresh status from the API (GET /api/v1/worktrees) and updates the badges in place.
Switching Worktrees
Click the Switch button on any worktree to activate it. When you switch worktrees:
- All running services are stopped
- Trellis reconfigures for the new worktree’s directory
- Services are restarted in the new worktree
- Any configured
pre_activatehooks run
Switching worktrees changes where Trellis looks for binaries, logs, and other paths that use the {{.Worktree}} template variable.
Creating Worktrees
Use the Create New Worktree form to create a new worktree:
- Enter a branch name (e.g.,
feature-x,bugfix-123) - Trellis creates:
- A new git branch from your default branch
- A new worktree directory at
../<project>-<branch>
- Optionally check Switch to new worktree to immediately activate it
The branch name must start with a letter or number and can contain letters, numbers, hyphens, and underscores.
Removing Worktrees
Click the Remove button to delete a worktree. You’ll be asked whether to also delete the associated git branch.
Note: You cannot remove the currently active worktree or the main project worktree.
Related
- Worktrees Concept — How Trellis uses worktrees
- Configuration: worktrees — Worktree configuration options
Status Page
URL: /status
The Status page provides an overview of all configured services and their current state. Use it to monitor, start, and stop services.
Service List
Services are organized into two collapsible sections:
Stopped Services
Shows services that are not currently running. This section is expanded by default so you can quickly see what needs attention.
For each stopped service:
- Name — The service name from your configuration
- Start button — Start this individual service
Running Services
Shows services that are currently running.
For each running service:
- Name — The service name
- PID — The process ID
- Uptime — How long the service has been running
- Stop button — Stop this individual service
- Restart button — Stop and restart the service
Bulk Actions
The header provides buttons to control all services at once:
- Start All — Start all stopped services
- Stop All — Stop all running services
- Refresh — Reload the current status from the server
Auto-Refresh
The page automatically refreshes service status periodically to keep the display current.
Related
- Services Concept — How Trellis manages services
- Configuration: services — Service configuration options
Trace Page
URL: /trace
The Trace page lets you execute distributed traces across multiple log sources. Use it to correlate events by trace ID, request ID, or any other pattern that appears in your logs.
Starting a Trace
To execute a trace:
- Trace ID — Enter the pattern to search for (e.g.,
req-abc123, a UUID, or any grep pattern) - Trace Group — Select which group of log viewers to search
- Time Range — Specify when to search:
- Range mode: Enter start and end times (e.g.,
1htonow, or6:00amto7:00am) - Day mode: Select a specific date to search that entire day
- Range mode: Enter start and end times (e.g.,
- Report Name — Optional custom name (auto-generated if empty)
- Click Execute

Trace Groups
Trace groups define which log sources to search together. Configure them in your trellis.hjson:
trace_groups: [
{
name: "backend"
log_viewers: ["api-logs", "worker-logs", "database-logs"]
}
{
name: "all"
log_viewers: ["api-logs", "worker-logs", "frontend-logs"]
}
]
Click the Groups button in the header to see configured groups and their log viewers.
Time Formats
The start and end time fields accept flexible formats:
- Relative:
1h,30m,2d(hours, minutes, days ago) - Clock time:
6:00am,14:30,9pm - Special:
now(current time)
Different timezone than production? If the machine running Trellis is in a different timezone than the hosts whose logs you’re tracing, set each log viewer’s
timezone(or alogging_defaults.timezonedefault) to the zone the production logs are written in — otherwise the time window is misaligned and traces come back empty or with the wrong hours. See config: Timezones.
Trace Reports
After executing a trace, a report is generated showing:
- All matching log entries from all log viewers in the group
- Sorted chronologically across sources
- Source column showing which log viewer each entry came from
- Entry details panel for inspecting individual entries

Reports are saved and listed in the Trace Reports table. Click a report name to view it, or delete old reports you no longer need.
Report Features
When viewing a trace report:
- Filter — Search within the results (supports
trace:textto expand matches by trace ID) - Timestamp toggle — Switch between absolute and relative timestamps
- Entry details — Click any row to see all fields with copy buttons
- Delete — Remove the report when done
Expand by ID
When Expand by ID is checked, Trellis searches for additional log entries that share extracted IDs (like request IDs or correlation IDs) from the initial results. This helps find related entries that may not contain the original trace pattern.
Related
- Configuration: trace_groups — Trace group configuration
- Configuration: log_viewers — Log viewer configuration
- trellis-ctl trace — CLI trace commands
Crashes Page
URL: /crashes
The Crashes page lists crash reports generated when services exit unexpectedly. Use it to investigate failures and debug issues.
Crash Report List
When a service crashes, Trellis captures:
- Name — Unique crash report ID (timestamp-based)
- Service — Which service crashed
- Trace ID — If the crash output contained a trace ID, it’s extracted and linked
- Exit Code — The process exit code (non-zero indicates error)
- Error — Summary of the error message or signal
- Created — When the crash occurred
Click on a crash report name to view the full details.
Crash Report Details
The detail view (/crashes/<id>) shows:
- Full error message — Complete error output
- Stack trace — If available, the full stack trace
- Last output — The final lines of stdout/stderr before the crash
- Environment — Service configuration at the time of crash
Managing Crash Reports
- Delete — Remove individual crash reports using the trash icon
- Clear All — Remove all crash reports at once
Crash reports are stored on disk in the .trellis/crashes/ directory and persist across Trellis restarts.
Trace ID Extraction
If your service logs include a trace ID or request ID when crashing, Trellis attempts to extract it. This allows you to:
- See the trace ID in the crash list
- Use the Trace page to search for related log entries across all services
Configure the trace ID pattern in your service’s crash settings if automatic extraction doesn’t work.
Related
- Configuration: crashes — Crash reporting configuration
- trellis-ctl crashes — CLI crash commands
Events Page
URL: /events
The Events page displays a chronological timeline of system events. Use it to understand what has happened in your development environment.
Event Types
Events are color-coded by type:
| Event Type | Color | Description |
|---|---|---|
service.started |
Green | A service started successfully |
service.restarted |
Green | A service was restarted (binary changed or manual restart) |
service.stopped |
Gray | A service was stopped |
service.crashed |
Red | A service exited unexpectedly |
workflow.started |
Gray | A workflow began execution |
workflow.finished |
Blue | A workflow completed |
worktree.activated |
Blue | The active worktree was changed |
claude.session.moved |
Blue | A Claude session was moved to a new worktree |
Event Details
Each event shows:
- Time — When the event occurred
- Type — The event type (with color-coded badge)
- Worktree — Which worktree the event occurred in
- Details — Additional context (service name, exit code, duration, etc.)
Event Retention
Events are kept in memory and reset when Trellis restarts. The page shows the most recent events first (scrolled to bottom).
Real-Time Updates
Click Refresh to reload the event list. For real-time event streaming, use the WebSocket API or subscribe via the event bus.
Related
- API: Events — Event API endpoints
- Go Client: Events — Programmatic event access
Reference
Configuration Reference
Trellis is configured via an HJSON file (JSON with comments and relaxed syntax).
Config File Location
Trellis searches for configuration in this order:
- Path specified with
-configflag trellis.hjsonin current directorytrellis.jsonin current directory
Complete Example
{
version: "1.0"
project: {
name: "myapp"
description: "My Application Development Environment"
}
server: {
port: 1234
host: "127.0.0.1"
}
// Reverse proxy (mirrors production routing)
proxy: [
{
listen: ":443"
tls_tailscale: true
routes: [
{ path_regexp: "^/api/.+", upstream: "localhost:3001" }
{ upstream: "localhost:3000" }
]
}
]
// Worktree configuration
worktree: {
discovery: {
mode: "git"
}
repo_dir: "/Users/dev/src/myapp"
create_dir: "/Users/dev/src"
binaries: {
path: "/Users/dev/bin/{{if .Worktree.Name}}{{.Worktree.Name}}{{else}}myapp{{end}}"
}
lifecycle: {
on_create: [
{ name: "npm-install", command: ["npm", "install"], timeout: "5m" }
{ name: "build", command: ["make", "build"], timeout: "10m" }
]
}
}
watch: {
debounce: "500ms"
}
terminal: {
backend: "tmux"
tmux: {
history_limit: 50000
shell: "/bin/zsh"
}
shortcuts: [
{ key: "cmd+l", window: "~prod-logs" }
]
remote_windows: [
{ name: "prod (1)", ssh_host: "prod01", tmux_session: "main" }
{ name: "prod (2)", ssh_host: "prod02", tmux_session: "main" }
{ name: "db01", command: ["ssh", "-t", "db01", "screen", "-dR", "db"] }
]
links: [
{ name: "admin", url: "http://localhost:8080/" }
{ name: "docs", url: "https://docs.example.com/" }
]
vscode: {
binary: "code-server"
port: 8443
}
}
crashes: {
reports_dir: ".trellis/crashes"
max_age: "7d"
max_count: 100
}
cases: {
dir: "trellis/cases"
}
trace: {
reports_dir: "traces"
max_age: "7d"
}
logging_defaults: {
parser: {
type: "json"
timestamp: "time"
level: "level"
id: "trace_id"
stack: "stack"
}
derive: {
ts_short: {
from: "time"
op: "timefmt"
args: { format: "15:04:05.000" }
}
file_line: {
op: "fmt"
args: { template: "{file}:{line}" }
}
}
layout: [
{ field: "ts_short", min_width: 12, max_width: 12, timestamp: true }
{ field: "level", min_width: 5 }
{ field: "file_line", max_width: 40 }
{ field: "msg", max_width: 80 }
]
}
trace_groups: [
{
name: "web"
log_viewers: ["web01-logs", "web02-logs"]
}
{
name: "api"
log_viewers: ["api01-logs", "api02-logs"]
}
]
log_viewers: [
{
name: "prod-logs"
source: {
type: "ssh"
host: "prod01"
path: "/var/log/myapp/"
current: "current"
rotated_pattern: "@*s"
decompress: "zstd -dc"
}
}
{
name: "web01-logs"
source: {
type: "ssh"
host: "web01"
path: "/var/log/web/"
current: "current"
rotated_pattern: "@*s"
decompress: "zstd -dc"
}
}
{
name: "web02-logs"
source: {
type: "ssh"
host: "web02"
path: "/var/log/web/"
current: "current"
rotated_pattern: "@*s"
decompress: "zstd -dc"
}
}
{
name: "api01-logs"
source: {
type: "ssh"
host: "api01"
path: "/var/log/api/"
current: "current"
rotated_pattern: "@*s"
decompress: "zstd -dc"
}
}
{
name: "api02-logs"
source: {
type: "ssh"
host: "api02"
path: "/var/log/api/"
current: "current"
rotated_pattern: "@*s"
decompress: "zstd -dc"
}
}
]
log_viewer_settings: {
idle_timeout: "5m"
disconnect_grace: "30s"
auto_pause_rate: 30
}
services: [
// Infrastructure (external binaries)
{
name: "redis"
command: ["redis-server"]
watching: false
}
// Core services
{
name: "api"
command: ["{{.Worktree.Binaries}}/api", "/etc/myapp/api.json"]
}
{
name: "web"
command: ["{{.Worktree.Binaries}}/web", "/etc/myapp/web.json"]
}
{
name: "worker"
command: ["{{.Worktree.Binaries}}/worker", "/etc/myapp/worker.json"]
}
]
workflows: [
{
id: "test"
name: "Run All Tests"
command: ["go", "test", "-json", "-count=1", "./..."]
output_parser: "go_test_json"
timeout: "10m"
}
{
id: "build"
name: "Build All"
command: ["make", "build"]
timeout: "10m"
output_parser: "go"
}
{
id: "db-reset"
name: "Reset Database"
commands: [
["./bin/dbutil", "reset"]
["./bin/dbutil", "seed"]
]
confirm: true
confirm_message: "This will delete all data. Continue?"
restart_services: true
}
{
id: "deploy"
name: "Deploy"
inputs: [
{ name: "environment", type: "select", label: "Environment", options: ["staging", "production"], default: "staging", required: true }
{ name: "deploy_date", type: "datepicker", label: "Deploy Date" }
{ name: "dry_run", type: "checkbox", label: "Dry run", default: false }
]
confirm: true
confirm_message: "Deploy to {{ .Inputs.environment }}?"
command: ["./deploy.sh", "--env={{ .Inputs.environment }}", "--date={{ .Inputs.deploy_date }}", "{{ if .Inputs.dry_run }}--dry-run{{ end }}"]
}
]
ui: {
theme: "auto"
notifications: {
enabled: true
events: ["service.crashed", "workflow.finished", "notify.done", "notify.error"]
failures_only: false
}
}
}
Section Reference
project
project: {
name: "myapp" // Project name (shown in UI)
description: "..." // Optional description
}
server
server: {
host: "100.80.99.38" // Bind address (use 0.0.0.0 for all interfaces)
port: 1234 // Server port
tls_tailscale: true // Automatic Tailscale HTTPS certificates
// Or use a static certificate pair instead:
// tls_cert: "path"
// tls_key: "path"
public_url: "https://mybox.tailnet.ts.net:1234" // External URL and TLS SNI name
allowed_origins: [ // Extra cross-origin browser origins permitted
"https://review.example.com"
]
}
| Field | Default | Description |
|---|---|---|
host |
"127.0.0.1" |
Bind address. Use "0.0.0.0" to allow remote access. |
port |
1234 |
Server port |
tls_tailscale |
false |
Fetch and automatically renew HTTPS certificates through the local Tailscale daemon. |
tls_cert |
(none) | Path to TLS certificate for HTTPS |
tls_key |
(none) | Path to TLS private key |
public_url |
(none) | External URL the UI is reachable at (e.g., behind a reverse proxy). Automatically permitted as a browser origin. |
allowed_origins |
[] |
Additional cross-origin browser origins permitted to call the API and open WebSockets. Loopback (localhost, 127.0.0.1, ::1) is always allowed. |
When host is loopback-only the server runs in DNS-rebinding-safe mode: requests are rejected unless the Host header is loopback or appears in allowed_origins/public_url. Binding to 0.0.0.0 (or any non-loopback address) is treated as opt-in to wide network access — the Host gate is relaxed, but Origin-based CORS still blocks browser-driven cross-origin attacks. List your external hostname in public_url (or allowed_origins) so a browser loading the UI from that address gets an Origin match.
tls_tailscale and tls_cert/tls_key are mutually exclusive. With
tls_tailscale: true, Trellis obtains certificates on demand from the local
Tailscale daemon and renews them automatically; no certificate files are
needed. Clients must connect using the machine’s *.ts.net hostname so the TLS
handshake includes the correct SNI name. Set public_url to that HTTPS URL when
host is an IP address or wildcard bind address.
proxy
Configures reverse proxy listeners for routing requests to backend services. Useful for mirroring production routing (e.g., Caddy/nginx) in development. WebSocket upgrades are handled automatically.
proxy: [
{
listen: ":1001"
tls_tailscale: true
routes: [
{ upstream: "localhost:1000" }
]
}
{
listen: ":443"
tls_tailscale: true
routes: [
{ path_regexp: "askws", upstream: "localhost:3000" }
{ path_regexp: "^/g/.+/chatws", upstream: "localhost:3002" }
{ path_regexp: "^/api/.+", upstream: "localhost:3001" }
{ upstream: "localhost:3000" }
]
}
]
Proxy listener fields:
| Field | Required | Description |
|---|---|---|
listen |
yes | Address to bind (e.g., ":443", "0.0.0.0:8080", ":1001") |
tls_tailscale |
no | Use Tailscale daemon for automatic TLS certificates. |
tls_cert |
no | Path to TLS certificate. Supports ~ expansion. |
tls_key |
no | Path to TLS private key. Supports ~ expansion. |
routes |
yes | Ordered list of route rules. First match wins. |
tls_tailscale and tls_cert/tls_key are mutually exclusive. When tls_tailscale is true, certificates are fetched automatically from the local Tailscale daemon — no cert files needed. This matches Caddy’s built-in Tailscale TLS behavior.
Route fields:
| Field | Required | Description |
|---|---|---|
path_regexp |
no | Regex to match against request path. Omit for catch-all. |
upstream |
yes | Target address (host:port). http:// prefix is optional. |
Routes are evaluated in order — the first matching route handles the request. A route without path_regexp matches all requests (catch-all). Place catch-all routes last.
Template variables ({{.Worktree.*}}) are supported in listen and upstream values.
worktree
worktree: {
repo_dir: "." // Directory for git worktree discovery
create_dir: ".." // Directory where new worktrees are created
discovery: {
mode: "git" // Discovery mode
}
binaries: {
path: "{{.Worktree.Root}}/bin" // Where binaries are built
}
lifecycle: {
on_create: [ // Run once when worktree is created
{ name: "setup", command: ["make", "setup"], timeout: "5m" }
]
pre_activate: [ // Run before each activation
{ name: "build", command: ["make", "build"], timeout: "2m" }
]
}
}
| Field | Default | Description |
|---|---|---|
repo_dir |
"." (current directory) |
Root directory for git worktree discovery |
create_dir |
".." (parent directory) |
Directory where new worktrees are created |
discovery.mode |
"git" |
Discovery mode. Currently only "git" is supported. |
binaries.path |
"{{.Worktree.Root}}/bin" |
Path to compiled binaries |
watch
watch: {
debounce: "100ms" // Wait for rapid file changes to settle
}
| Field | Default | Description |
|---|---|---|
debounce |
"100ms" |
Time to wait for rapid file changes to settle before triggering a restart |
logging
Configures Trellis application logging (not service logs):
logging: {
level: "info" // "debug", "info", "warn", "error"
format: "json" // "json", "text"
}
services
services: [
{
// Required
name: "service-name"
command: "./bin/app" // String or array
// Optional
args: ["-port", "8080"] // Arguments (if command is string)
work_dir: "{{.Worktree.Root}}" // Working directory
env: { KEY: "value" } // Environment variables
watch_binary: "path" // Binary to watch for restarts
watch_files: ["config.yaml"] // Additional files to watch
enabled: true // Enable/disable the service
watching: true // Include in binary watching
depends_on: ["postgres"] // Services that must start first
// Restart policy (top-level fields)
restart_policy: "on-failure" // "always", "on-failure", "never"
max_restarts: 3 // Give up after N attempts
restart_delay: "1s" // Wait between restarts
// Or use nested restart block for policy only
restart: {
policy: "on-failure"
}
// Graceful shutdown
stop_signal: "SIGTERM" // Signal to send (default: SIGTERM)
stop_timeout: "10s" // Wait before SIGKILL
// Log buffer size (default: 1000)
log_buffer_size: 10000
// Log parsing and display
logging: {
parser: {
type: "json"
timestamp: "ts"
level: "level"
message: "msg"
id: "request_id"
stack: "stack"
}
// Derived fields computed from parsed fields
derive: {
short_time: { from: "timestamp", op: "timefmt", args: { format: "15:04:05" } }
}
// Column layout (overrides logging_defaults)
layout: [
{ field: "short_time", min_width: 8 }
{ field: "level", min_width: 5 }
{ field: "message", max_width: 0 }
]
}
}
]
workflows
workflows: [
{
// Required
id: "workflow-id"
name: "Workflow Name"
description: "Description for CLI help" // Shown in trellis-ctl workflow list/describe
command: ["make", "build"] // Single command
// Or multiple commands (run sequentially)
commands: [
["make", "clean"],
["make", "build"]
]
// Optional
timeout: "10m"
output_parser: "go" // "go", "go_test_json", "generic", "html", "none"
confirm: false // Require confirmation
confirm_message: "Are you sure?"
requires_stopped: ["api"] // Services to stop first
restart_services: false // Restart watched services after
// Input parameters (prompts user before execution)
inputs: [
{
name: "environment" // Variable name for templates
type: "select" // "text", "select", "checkbox", or "datepicker"
label: "Target Environment"
description: "Target deployment environment"
options: ["staging", "production"]
default: "staging"
required: true
}
{
name: "version"
type: "text"
label: "Version Tag"
description: "Semantic version tag"
placeholder: "e.g., v1.2.3"
pattern: "^v[0-9]+\\.[0-9]+\\.[0-9]+$" // Validation pattern
}
{
name: "deploy_date"
type: "datepicker"
label: "Deploy Date"
description: "Scheduled deployment date"
// default: "2026-01-15" // Optional, defaults to today
}
{
name: "dry_run"
type: "checkbox"
label: "Dry run (don't actually deploy)"
description: "Preview changes without applying"
default: false
}
]
}
]
Workflow Inputs
Workflows can define input parameters that prompt the user with a dialog before execution:
| Input Type | Description | Fields |
|---|---|---|
text |
Free-form text input | placeholder, default, required, description, pattern, allowed_values |
select |
Dropdown with predefined options | options (array), default, required, description |
checkbox |
Boolean toggle | default (bool), description |
datepicker |
Date selector | default (YYYY-MM-DD string), required, description |
Validation fields (for CLI/automation safety):
| Field | Description |
|---|---|
description |
Description shown in trellis-ctl workflow describe output |
pattern |
Regex pattern that values must match |
allowed_values |
Whitelist of allowed values (rejects anything else) |
Input values are available in command and confirm_message templates via {{ .Inputs.name }}:
{
id: "deploy"
name: "Deploy"
inputs: [
{ name: "env", type: "select", options: ["staging", "prod"], required: true }
{ name: "scheduled_date", type: "datepicker", label: "Scheduled Date" }
{ name: "dry_run", type: "checkbox", label: "Dry run", default: false }
]
confirm: true
confirm_message: "Deploy to {{ .Inputs.env }} on {{ .Inputs.scheduled_date }}?"
command: ["./deploy.sh", "--env={{ .Inputs.env }}", "--date={{ .Inputs.scheduled_date }}", "{{ if .Inputs.dry_run }}--dry-run{{ end }}"]
}
The datepicker defaults to today’s date if no default is specified. Date values are passed as YYYY-MM-DD strings (e.g., 2026-01-15).
terminal
terminal: {
backend: "tmux"
tmux: {
history_limit: 50000
shell: "/bin/sh" // Default shell
}
remote_windows: [
// Option 1: SSH host + tmux session (auto-builds command)
{
name: "admin(1)"
ssh_host: "admin.example.com"
tmux_session: "main"
}
// Option 2: Explicit command
{
name: "prod"
command: ["ssh", "-t", "prod.example.com", "tmux", "attach"]
}
]
// Custom keyboard shortcuts to terminals
shortcuts: [
{ key: "cmd+1", window: "#api" } // Jump to service
{ key: "cmd+2", window: "~nginx-logs" } // Jump to log viewer
{ key: "cmd+3", window: "!admin(1)" } // Jump to remote
]
vscode: {
binary: "code-server"
port: 8443
user_data_dir: "~/.config/code-server"
}
links: [
{ name: "Grafana", url: "http://localhost:3000/" }
]
}
log_viewers
log_viewers: [
{
name: "nginx-logs"
mode: "live" // "live" (default) or "explore" — see Modes below
timezone: "America/New_York" // IANA zone the remote host writes timestamps in
// (inherits logging_defaults.timezone; see Timezones below)
source: {
type: "ssh" // "file", "ssh", "command", "docker", "kubernetes"
// Note: "service" sources are auto-generated for services with parsers
host: "web01.example.com"
path: "/var/log/nginx" // Log directory
current: "access.log" // Active log file name
rotated_pattern: "access.log.*" // Pattern for rotated logs
decompress: "zcat" // Command to decompress rotated files
follow: true // Follow log output (default: true)
since: "1h" // How far back to start
}
parser: {
type: "json" // "json", "logfmt", "regex", "syslog", "none"
timestamp: "time"
level: "status"
message: "request"
id: "request_id"
}
// Derived fields computed from parsed fields
derive: {
short_time: { from: "timestamp", op: "timefmt", args: { format: "15:04:05" } }
}
// Column layout (overrides logging_defaults)
layout: [
{ field: "short_time", min_width: 8 }
{ field: "level", min_width: 5 }
{ field: "message", max_width: 0 }
]
buffer: {
max_entries: 10000 // Max entries in memory
}
}
]
// Global log viewer settings
log_viewer_settings: {
idle_timeout: "5m" // Stop viewers not accessed at all after this duration ("0" disables)
disconnect_grace: "30s" // Stop the tail this long after the last watcher disconnects ("0" disables)
auto_pause_rate: 30 // Lines/sec that triggers UI auto-pause in the browser (0 disables)
}
Modes:
| Mode | Behavior |
|---|---|
live (default) |
Opening the viewer starts tailing the source immediately and follows new entries. |
explore |
For high-volume logs (e.g. nginx access logs). Opening the viewer does not start the tail — the server loads a static snapshot of the ~200 most recent lines read directly from the end of the file (a byte-offset backward read), and the UI opens paused with search/scrollback as the primary workflow. A Go live button in the header starts the tail and switches to streaming. Scrolling up to page back through history, and history search, work the same as in live mode. |
explore mode requires a source that supports backward reads — file and ssh. For docker, kubernetes, and command sources, an explore-mode viewer falls back to starting the tail immediately but still opens paused, so the UI behaves the same even though the tail is already running underneath. mode is validated at config load; the only accepted values are "live", "explore", or unset.
Log viewer defaults:
| Field | Default | Description |
|---|---|---|
mode |
"live" |
"live" or "explore" — see Modes above |
timezone |
trellis host’s local zone | IANA name of the zone the remote host writes its log timestamps in; inherits logging_defaults.timezone — see Timezones below |
source.follow |
true |
Follow log output in real-time |
source.since |
"1h" |
How far back to start reading when connecting |
buffer.max_entries |
10000 |
Maximum entries to keep in memory |
log_viewer_settings.idle_timeout |
"5m" |
Stop viewers that haven’t been accessed at all in this long ("0" disables) |
log_viewer_settings.disconnect_grace |
"30s" |
Stop the tail this long after the last watcher (WebSocket subscriber) disconnects — e.g. when you navigate away from the page. Reconnecting within the grace period keeps the tail warm. REST API polling also counts as activity and keeps the tail alive. ("0" disables) |
log_viewer_settings.auto_pause_rate |
30 |
Lines/sec above which the UI automatically drops out of following in the browser, to avoid rendering every line of a burst (0 disables) |
trace
trace: {
reports_dir: "traces"
max_age: "7d"
}
trace_groups: [
{
name: "api-flow"
log_viewers: ["nginx-logs", "api-logs", "db-logs"]
}
]
Auto-generated services group: When services have logging.parser configured (directly or via logging_defaults), Trellis automatically creates svc:* log viewers and a services trace group. Use trellis-ctl trace <id> services -since 1h to search across dev service logs with no additional configuration. If you define a services trace group in config, the auto-generated viewers are appended to it.
crashes
crashes: {
reports_dir: ".trellis/crashes"
max_age: "7d"
max_count: 100
}
cases
cases: {
dir: "trellis/cases" // Cases directory relative to worktree root
}
| Field | Default | Description |
|---|---|---|
dir |
"trellis/cases" |
Directory for case storage, relative to worktree root. Archived cases are stored in a sibling -archived directory (e.g., trellis/cases-archived/). |
agent
agent: {
install_skill: true // Install the trellis skill file for coding agents
}
| Field | Default | Description |
|---|---|---|
install_skill |
true |
Whether Trellis installs its skill file at .claude/skills/trellis/SKILL.md in the repo and each worktree (on startup and on worktree creation), teaching coding agents to use trellis-ctl. Installed copies carry a managed-by: trellis marker and are refreshed when the bundled skill changes; copies without the marker (user-edited) are never touched. |
logging_defaults
logging_defaults: {
timezone: "America/New_York" // Default production log timezone (IANA name)
// for every viewer that doesn't set its own
parser: {
type: "json"
timestamp: "ts"
level: "level"
message: "msg"
id: "request_id"
stack: "stack"
file: "source"
line: "lineno"
}
// Derived fields computed from parsed fields
derive: {
short_time: { from: "timestamp", op: "timefmt", args: { format: "15:04:05" } }
}
// Default column layout
layout: [
{ field: "short_time", min_width: 8 }
{ field: "level", min_width: 5 }
{ field: "message", max_width: 0 } // 0 = fill remaining
]
}
Timezones
Historical log search — scrollback paging and /trace — filters remote log files by a time window. For ssh sources, Trellis narrows the search by grepping the remote files for the hours/days in range, and it must express those in the timezone the remote host writes its timestamps in. Set that with timezone (an IANA name like America/New_York or UTC), per-viewer or as a logging_defaults.timezone default.
When timezone is unset, Trellis assumes the logs are in the trellis host’s own local timezone — correct only when trellis runs in the same zone as the production hosts. If it doesn’t (e.g. trellis on a UTC server, logs in US/Eastern), traces come back empty or with the wrong hours until you set timezone. The value also interprets log timestamps that carry no offset of their own; timestamps that already include an offset or Z are unaffected. Invalid IANA names are rejected at config load.
events
events: {
// Event history settings
history: {
max_events: 10000 // Maximum events to keep in memory
max_age: "1h" // Maximum age of events to keep
}
// Webhooks to notify on events
webhooks: [
{
id: "slack"
url: "https://hooks.slack.com/services/..."
events: ["service.crashed", "workflow.finished"]
}
]
}
ui
ui: {
theme: "auto" // "light", "dark", "auto"
log_terminal: "dev" // Default terminal window for Cmd+L jump
terminal: {
font_family: "Monaco, monospace"
font_size: 14
cursor_blink: true
}
notifications: {
enabled: true
events: ["service.crashed", "workflow.finished"]
failures_only: true
sound: false
}
editor: {
// For remote Trellis, enables vscode-remote:// URLs
remote_host: "devbox.example.com"
}
}
Template Variables
| Variable | Description |
|---|---|
{{.Project.Name}} |
Project name from config |
{{.Project.Root}} |
Project root directory |
{{.Worktree.Root}} |
Worktree root directory |
{{.Worktree.Branch}} |
Current branch name |
{{.Worktree.Binaries}} |
Configured binaries path |
{{.Worktree.Name}} |
Worktree directory name |
{{.Service.Name}} |
Current service name |
{{.Inputs.<name>}} |
Workflow input value (in workflow commands/confirm_message only) |
Template Functions
| Function | Description | Example |
|---|---|---|
slugify |
Convert to slug | {{.Branch | slugify}} |
replace |
String replace | {{.Name | replace "-" "_"}} |
upper |
Uppercase | {{.Name | upper}} |
lower |
Lowercase | {{.Name | lower}} |
default |
Default value | {{.Port | default 8080}} |
quote |
Shell quote | {{.Path | quote}} |
trellis-ctl Reference
trellis-ctl is the command-line tool for controlling a running Trellis instance.
Installation
# Build with Trellis
make build
# Or build directly
go build -o trellis-ctl ./cmd/trellis-ctl
Configuration
trellis-ctl resolves the API URL in this order (highest precedence first):
TRELLIS_APIenvironment variable — set automatically in Trellis-managed tmux sessions, so commands run inside Trellis terminals always reach the right instance.-config <path>flag orTRELLIS_CONFIGenvironment variable — load the specifiedtrellis.hjsonand use itsserver.public_urlif set; otherwise build the URL fromserver.host,server.port, and the configured TLS mode (server.tls_tailscaleorserver.tls_cert/server.tls_key).trellis.hjson(ortrellis.json) auto-discovered by walking up from the current directory, resolved the same way.http://localhost:1234fallback if no config or env override is found.
server.public_url, when set, is used verbatim — useful when the bind
address differs from the address clients should connect to (e.g., binding
to a Tailscale IP whose certificate is issued for the machine’s Tailscale
hostname rather than the IP). Example:
server: {
host: "100.80.99.38" // bind address
port: 1234
tls_cert: "..."
tls_key: "..."
public_url: "https://mybox.tail-net.ts.net:1234" // address clients use
}
When public_url is not set: if server.host is 0.0.0.0 or ::,
trellis-ctl connects to 127.0.0.1 instead, and when tls_tailscale or both
tls_cert and tls_key are set the scheme becomes https. Because Tailscale
TLS requires the machine’s *.ts.net SNI hostname, set public_url when using
tls_tailscale with an IP or wildcard bind address.
The same resolution is used by the Trellis daemon to compute the
TRELLIS_API value injected into Trellis-managed tmux sessions, so the CLI
inside terminals reaches the instance via the correct URL automatically.
| Environment Variable | Description |
|---|---|
TRELLIS_API |
Base URL of Trellis API; overrides config-based discovery. |
TRELLIS_CONFIG |
Path to trellis.hjson; used when -config is not given. |
Global Flags
| Flag | Description |
|---|---|
-json |
Output in JSON format |
-config <path> |
Path to trellis.hjson to read connection info from. |
The -json flag works with any command:
trellis-ctl -json status
trellis-ctl status -json
Commands
Service Commands
# List all services
trellis-ctl status
# Get specific service status
trellis-ctl status <service>
# Control services
trellis-ctl start <service>
trellis-ctl stop <service>
trellis-ctl restart <service>
Example output:
SERVICE STATE PID RESTARTS ERROR
backend running 12345 0
frontend running 12346 0
worker crashed - 3 exit code 1
Log Commands
# Basic log viewing
trellis-ctl logs <service> # Last 100 lines
trellis-ctl logs <service> -n 50 # Last 50 lines
trellis-ctl logs <service> -f # Follow mode
# Log viewers
trellis-ctl logs -viewer <name> # View log viewer
trellis-ctl logs -list # List available viewers
# Time filtering
trellis-ctl logs <service> -since 1h
trellis-ctl logs <service> -since 30m
trellis-ctl logs <service> -since 6:30am
trellis-ctl logs <service> -since 6:00am -until 7:00am
# Level filtering
trellis-ctl logs <service> -level error
trellis-ctl logs <service> -level warn,error
trellis-ctl logs <service> -level info+
# Pattern filtering
trellis-ctl logs <service> -grep "pattern"
trellis-ctl logs <service> -grep "panic|fatal"
trellis-ctl logs <service> -field host=prod1
# Context lines
trellis-ctl logs <service> -grep "error" -B 5 # 5 lines before
trellis-ctl logs <service> -grep "error" -A 10 # 10 lines after
trellis-ctl logs <service> -grep "error" -C 3 # 3 lines both
# Output formats
trellis-ctl logs <service> -json
trellis-ctl logs <service> -jsonl
trellis-ctl logs <service> -csv
trellis-ctl logs <service> -raw
trellis-ctl logs <service> -format "{{.timestamp}} [{{.level}}] {{.message}}"
# Management
trellis-ctl logs <service> -clear
trellis-ctl logs <service> -stats
Workflow Commands
# List workflows
trellis-ctl workflow list
# Describe a workflow (show inputs and validation)
trellis-ctl workflow describe <workflow-id>
# Run a workflow (waits for completion)
trellis-ctl workflow run <workflow-id>
# Run a workflow with inputs
trellis-ctl workflow run <workflow-id> --input1=value1 --input2=value2
# Check status of a running workflow (uses run ID, not workflow ID)
# The run ID is returned by "workflow run" when using -json
trellis-ctl workflow status <run-id>
# Cancel a running workflow. Accepts a run ID, or a workflow ID to cancel
# that workflow's most recent in-flight run.
trellis-ctl workflow cancel <id>
Structured results: When a workflow has an output_parser configured (e.g. go_compiler, go_test_json), completed runs include a Summary rollup in the -json status output — error/warning counts, test pass/fail/skip counts, and the names of failing tests:
"Summary": {
"Errors": 0,
"Warnings": 0,
"TestsPassed": 41,
"TestsFailed": 2,
"TestsSkipped": 1,
"FailedTests": ["github.com/acme/pkg.TestFoo", "github.com/acme/pkg.TestBar"],
"FirstError": ""
}
Summary is null for workflows without an output parser. The human-readable workflow run output prints the same rollup as a Summary: line plus a FAIL line per failing test.
Example: Discovering and running a workflow with inputs
# See available workflows
$ trellis-ctl workflow list
ID NAME DESCRIPTION
find-email Find Email Search for an email by message ID or database ID
db-fetch DB Fetch Fetch a database row by table and ID
# Get details about a workflow's inputs
$ trellis-ctl workflow describe find-email
Workflow: find-email
Name: Find Email
Description: Search for an email by message ID or database ID
Inputs:
--msgid Email message ID (optional)
Pattern: ^[a-zA-Z0-9._@<>-]+$
--id Database ID (optional)
Pattern: ^[0-9]+$
--date Date to search (required)
Type: datepicker (YYYY-MM-DD format)
# Run with validated inputs
$ trellis-ctl workflow run find-email --date=2024-01-15 --id=12345
{"email": "...", ...}
Input validation: When pattern or allowed_values are configured for an input, the server validates inputs before execution. Invalid inputs return an error without running the workflow.
Worktree Commands
# List worktrees
trellis-ctl worktree list
# Activate a worktree
trellis-ctl worktree activate <name>
Example output:
NAME BRANCH ACTIVE STATUS PATH
myproject main * ready /Users/dev/src/myproject
myproject-feature feature ready /Users/dev/src/myproject-feature
Event Commands
# Show recent events
trellis-ctl events # Last 50
trellis-ctl events -n 20 # Last 20
Crash Commands
# List crashes
trellis-ctl crash list
# Show most recent crash
trellis-ctl crash newest
# Show specific crash
trellis-ctl crash <id>
# Delete a crash
trellis-ctl crash delete <id>
# Clear all crashes
trellis-ctl crash clear
Trace Commands
# Execute a trace
trellis-ctl trace <id> <group> [options]
# Options
-since <time> # Start time (1h, 30m, 6:00am, 2026-01-10)
-until <time> # End time (default: now)
-name <name> # Report name
-no-expand-by-id # Disable ID expansion
# Examples
trellis-ctl trace abc123 api-flow -since 1h
trellis-ctl trace "user-456" auth-flow -since 6:00am -until 7:00am
trellis-ctl trace abc123 api-flow -since 2026-01-10 -until 2026-01-10
# Trace across dev service logs (auto-generated group)
trellis-ctl trace "req-123" services -since 1h
# View reports
trellis-ctl trace-report -list
trellis-ctl trace-report <name>
trellis-ctl trace-report <name> -json
trellis-ctl trace-report -groups # Shows auto-generated "services" group
trellis-ctl trace-report -delete <name>
Notify Command
Send notifications to alert users:
# Task completed (default)
trellis-ctl notify "Refactoring complete"
# Waiting for input
trellis-ctl notify "Need database credentials" -type blocked
# Error occurred
trellis-ctl notify "Build failed with 3 errors" -type error
| Type | Use Case |
|---|---|
done |
Task completed, user can review |
blocked |
Need user input to continue |
error |
Something failed |
Other Commands
trellis-ctl version # Show version
trellis-ctl help # Show help
Common Patterns
After Code Changes
# Check if services restarted
trellis-ctl status
# If crashed, view the crash
trellis-ctl crash newest
Debugging a Crash
# View most recent crash
trellis-ctl crash newest
# View crash history
trellis-ctl crash list
# View specific crash
trellis-ctl crash <id>
Running Builds
# Run build workflow
trellis-ctl workflow run build
# Check services restarted
trellis-ctl status
Searching Logs
# Find errors in last hour
trellis-ctl logs backend -level error -since 1h
# Search for pattern with context
trellis-ctl logs backend -grep "timeout" -C 5
# Export as JSON for analysis
trellis-ctl logs backend -since 1h -json > logs.json
Claude Code Integration
Trellis installs its skill file automatically: on startup it writes .claude/skills/trellis/SKILL.md into the repo and every worktree (and into new worktrees as they’re created), so Claude Code discovers trellis-ctl without any setup. Installed copies carry a managed-by: trellis marker and are refreshed when the bundled skill changes after an upgrade; remove the marker line to take ownership of a copy, or disable installation entirely:
agent: {
install_skill: false
}
The skill teaches Claude to check service status, read and filter logs, run validation workflows (and read their structured Summary results), inspect crashes, and run distributed traces. See Installation for manual setup (including Codex’s AGENTS.md).
Go Client Library
Trellis provides an official Go client library at pkg/client for programmatic access to the API. The library provides typed access to all endpoints and is used internally by trellis-ctl.
Installation
go get github.com/wingedpig/trellis/pkg/client
Basic Usage
package main
import (
"context"
"fmt"
"log"
"github.com/wingedpig/trellis/pkg/client"
)
func main() {
// Create a client (default Trellis port is 1234)
c := client.New("http://localhost:1234")
ctx := context.Background()
// List all services
services, err := c.Services.List(ctx)
if err != nil {
log.Fatal(err)
}
for _, svc := range services {
fmt.Printf("%s: %s\n", svc.Name, svc.Status.State)
}
// Start a service
svc, err := c.Services.Start(ctx, "backend")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Started %s (PID: %d)\n", svc.Name, svc.Status.PID)
}
API Versioning
The client supports Stripe-style date-based API versioning. By default, the latest version is used. Pin to a specific version for stability:
c := client.New("http://localhost:1234", client.WithVersion("2026-01-17"))
The version is sent via the Trellis-Version HTTP header on each request.
Configuration Options
import "time"
c := client.New("http://localhost:1234",
client.WithVersion("2026-01-17"), // Pin API version
client.WithTimeout(60 * time.Second), // Custom timeout (default: 30s)
client.WithHTTPClient(customClient), // Custom http.Client
)
Available Sub-Clients
| Sub-Client | Description |
|---|---|
c.Services |
Service management (list, get, start, stop, restart, logs) |
c.Worktrees |
Worktree operations (list, get, activate, remove) |
c.Workflows |
Workflow execution (list, get, run, status) |
c.Events |
Event log access (list with filters) |
c.Logs |
Log viewer operations (list viewers, get entries, history) |
c.Trace |
Distributed tracing (execute, list/get/delete reports, list groups) |
c.Crashes |
Crash history (list, get, newest, delete, clear) |
c.Notify |
Notifications (send) |
Service Operations
// List all services
services, _ := c.Services.List(ctx)
// Get a specific service
svc, _ := c.Services.Get(ctx, "backend")
// Start/stop/restart
svc, _ := c.Services.Start(ctx, "backend")
svc, _ := c.Services.Stop(ctx, "backend")
svc, _ := c.Services.Restart(ctx, "backend")
// Get log buffer (raw JSON)
logs, _ := c.Services.Logs(ctx, "backend", 100)
// Clear log buffer
_ = c.Services.ClearLogs(ctx, "backend")
Worktree Operations
// List all worktrees
worktrees, _ := c.Worktrees.List(ctx)
// Get a specific worktree
wt, _ := c.Worktrees.Get(ctx, "feature-branch")
// Activate a worktree (switches active environment)
result, _ := c.Worktrees.Activate(ctx, "feature-branch")
fmt.Printf("Activated %s in %s\n", result.Worktree.Name(), result.Duration)
// Remove a worktree
_ = c.Worktrees.Remove(ctx, "old-branch", &client.RemoveOptions{
DeleteBranch: true, // Also delete the git branch
})
Workflow Operations
// List workflows
workflows, _ := c.Workflows.List(ctx)
// Run a workflow (returns immediately)
status, _ := c.Workflows.Run(ctx, "build", nil)
// Run in a specific worktree
status, _ := c.Workflows.Run(ctx, "build", &client.RunOptions{
Worktree: "feature-branch",
})
// Poll for completion
for status.State == client.WorkflowStateRunning {
time.Sleep(500 * time.Millisecond)
status, _ = c.Workflows.Status(ctx, status.ID)
}
if status.Success {
fmt.Println("Workflow completed successfully")
fmt.Println(status.Output)
}
// Structured rollup of parsed output (nil without an output parser)
if s := status.Summary; s != nil {
fmt.Printf("tests: %d passed, %d failed\n", s.TestsPassed, s.TestsFailed)
for _, name := range s.FailedTests {
fmt.Println("FAIL", name)
}
}
// Cancel a running workflow (run ID, or workflow ID for its latest run)
status, _ = c.Workflows.Cancel(ctx, "build")
Event Operations
// List recent events
events, _ := c.Events.List(ctx, &client.ListOptions{
Limit: 50,
})
// Filter by type and time range
events, _ := c.Events.List(ctx, &client.ListOptions{
Types: []string{"service.started", "service.stopped"},
Since: time.Now().Add(-1 * time.Hour),
Worktree: "main",
})
Log Viewer Operations
// List configured log viewers
viewers, _ := c.Logs.List(ctx)
// Get entries from live buffer
entries, _ := c.Logs.GetEntries(ctx, "nginx", &client.LogEntriesOptions{
Limit: 100,
Since: time.Now().Add(-1 * time.Hour),
Level: "error",
})
// Get historical entries (from rotated files)
entries, _ := c.Logs.GetHistoryEntries(ctx, "nginx", &client.LogEntriesOptions{
Grep: "connection refused",
Before: 3, // Context lines before match
After: 3, // Context lines after match
})
Distributed Tracing
// Execute a trace query
result, _ := c.Trace.Execute(ctx, &client.TraceRequest{
ID: "req-abc123", // Correlation ID to search for
Group: "web", // Trace group (set of log viewers)
Start: time.Now().Add(-1 * time.Hour),
End: time.Now(),
ExpandByID: true, // Two-pass search for related entries
})
// Poll for completion
for {
report, _ := c.Trace.GetReport(ctx, result.Name)
if report.Status == "completed" {
fmt.Printf("Found %d entries\n", report.Summary.TotalEntries)
for _, entry := range report.Entries {
fmt.Printf("[%s] %s: %s\n", entry.Source, entry.Level, entry.Message)
}
break
}
time.Sleep(500 * time.Millisecond)
}
// List saved reports
reports, _ := c.Trace.ListReports(ctx)
// Delete a report
_ = c.Trace.DeleteReport(ctx, "trace-2026-01-17-abc123")
// List trace groups
groups, _ := c.Trace.ListGroups(ctx)
Notifications
// Send a notification
_, _ = c.Notify.Send(ctx, "Build complete!", client.NotifyDone)
_, _ = c.Notify.Send(ctx, "Waiting for input", client.NotifyBlocked)
_, _ = c.Notify.Send(ctx, "Build failed", client.NotifyError)
Error Handling
API errors are returned as *client.APIError:
svc, err := c.Services.Get(ctx, "unknown-service")
if err != nil {
if apiErr, ok := err.(*client.APIError); ok {
fmt.Printf("API error: %s - %s\n", apiErr.Code, apiErr.Message)
// Common codes: "not_found", "invalid_request", "conflict"
}
}
Types Reference
Key types exported by the client library:
| Type | Description |
|---|---|
Service |
Service definition and status |
ServiceStatus |
Runtime state (State, PID, ExitCode, etc.) |
Worktree |
Git worktree info (Path, Branch, Commit, Dirty, etc.) |
Workflow |
Workflow definition (ID, Name, Command, etc.) |
WorkflowStatus |
Execution status (State, Success, Output, Summary, etc.) |
WorkflowSummary |
Structured rollup of parsed output (error/test counts, failed test names) |
Event |
Event log entry (Type, Timestamp, Payload) |
LogViewer |
Log viewer definition (Name, Description) |
LogEntry |
Parsed log entry (Timestamp, Level, Message, Fields) |
TraceRequest |
Trace query parameters |
TraceReport |
Complete trace results with entries |
TraceGroup |
Group of log viewers for tracing |
Documentation
For complete API reference, see:
go doc github.com/wingedpig/trellis/pkg/client
Keyboard Shortcuts
Trellis provides keyboard shortcuts throughout the web interface for quick navigation and control.
Global Shortcuts
These shortcuts work from any page in Trellis:
| Shortcut | Action |
|---|---|
Cmd/Ctrl + P |
Open navigation picker |
Shift + Cmd/Ctrl + P |
Open command palette (actions) |
Cmd/Ctrl + / |
Open workflow picker (local terminals only) |
Cmd/Ctrl + Backspace |
Open history picker (recently visited screens) |
Cmd/Ctrl + H |
Open commands & shortcuts menu |
Commands & Shortcuts Menu
Click the keyboard icon in the top nav (or press Cmd/Ctrl + H) to open a tappable menu listing every action — useful on touch devices where modifier-key shortcuts aren’t practical. Each row runs its action when tapped and dismisses the menu.
The menu lists:
- Open navigation picker — same as
Cmd/Ctrl + P - Open history picker — same as
Cmd/Ctrl + Backspace. If no history has been recorded yet in the current tab session, an alert says so. - On the terminal page additionally: Open workflow picker (when a workflow selector is visible), Toggle Terminal / Code view (when a local worktree is active), and Open links panel (when links are configured)
- Custom shortcuts configured for the current worktree (each appears with the assigned key combo as a label)
Custom shortcuts invoked from the menu run the same handler as the keyboard path, so the target screen is resolved and navigated to identically.
Navigation Picker (Cmd+P)
The navigation picker provides quick access to all destinations:
| Prefix | Type | Example |
|---|---|---|
/ |
Pages | / Status, / Worktrees, / Trace, / Events, / Usage |
@ |
Local terminals | @main - dev, @feature-auth - claude |
! |
Remote terminals | !admin(1) |
# |
Services | #api, #worker |
~ |
Log viewers | ~nginx-logs, ~api-logs |
> |
External links | > Grafana, > Docs |
Type to filter/search destinations. Press Enter to select, Escape to cancel.
Command Palette (Shift+Cmd+P)
While the navigation picker is for destinations, the command palette is for actions (VS Code style). Titles are in Category: verb form, so typing a category prefix narrows the list:
- Worktree: Create new…, Activate
<name>, Remove<name> - Service: Start/Stop/Restart
<name>, Start all, Stop all, Clear logs - Workflow: Run
<name>(scoped to the current worktree) - Claude: / Codex: New session in
<worktree> - Crash: Clear all
- View: Copy current URL, Token usage & costs
- Help: Show keyboard shortcuts
Destructive commands ask for confirmation. Type to filter, Enter to run, Escape to cancel.
History Picker (Cmd+Backspace)
Quickly return to recently visited screens:
- Shows up to 50 recent entries
- Most recent first (the screen you just left appears at top)
- Current screen is excluded
- History is shared across all pages via session storage
Use arrow keys to navigate, Enter to select, Escape to cancel.
Links Panel (Cmd+K)
On the terminal page, Cmd/Ctrl + K opens a standalone list of your configured external links (terminal.links) — the same entries the navigation picker shows with the > prefix, gathered in one place. Click a link to open it in a reusable named tab (re-opening focuses the existing tab rather than duplicating it). The shortcut is active only when links are configured.
Claude Shortcuts
When on the Claude chat page:
| Shortcut | Action |
|---|---|
Enter |
Send message |
Shift + Enter |
Insert newline without sending |
Escape |
Stop/cancel current response |
Terminal Shortcuts
When focused on a terminal:
| Shortcut | Action |
|---|---|
Cmd/Ctrl + K |
Open links panel (when links are configured) |
Cmd/Ctrl + E |
Open VS Code editor for current worktree |
Ctrl + Escape |
Return from editor iframe to terminal (same-origin only) |
Shift + Enter |
Insert newline without executing command |
Editor Toggle (Cmd+E)
Quickly switch between the terminal and VS Code:
- From terminal: Opens VS Code for the current worktree
- From editor: Returns to the terminal view
- Hidden for remote terminals (no associated worktree)
Configuration
Customize terminal appearance in your config:
{
ui: {
terminal: {
font_family: "Monaco, monospace"
font_size: 14
cursor_blink: true
}
}
}
State and Files
Trellis stores runtime state in the following locations.
Per-Worktree State
These directories are created relative to each worktree’s root:
| Directory | Contents | Cleanup |
|---|---|---|
.trellis/crashes/ |
Crash reports (JSON) | trellis-ctl crash clear |
traces/ |
Trace reports (JSON) | trellis-ctl trace-report -delete <name> |
Crash reports are automatically cleaned up after 7 days or when the count exceeds 100 (configurable via crashes.max_age and crashes.max_count).
tmux Sessions
Trellis creates tmux sessions named <project> for the main worktree and <project>-<branch> for additional worktrees. For example, if your project is myapp with worktrees on main and feature:
# List Trellis-created sessions
tmux list-sessions | grep myapp
# Kill a specific session
tmux kill-session -t myapp-feature
# Kill all project sessions
tmux kill-server # Warning: kills ALL tmux sessions
Temporary Files
Terminal WebSocket connections use named pipes in /tmp/:
/tmp/trellis-pipe-<session>-<window>-<timestamp>.fifo
These are cleaned up automatically when connections close.
Full Reset
To completely reset Trellis state:
# Stop Trellis
# (Ctrl+C or kill the process)
# Remove per-worktree state
rm -rf .trellis/ traces/
# Kill tmux sessions for this project
tmux kill-session -t myproject
tmux kill-session -t myproject-feature # etc.
# Restart Trellis
./trellis
Related
- Installation — Setting Trellis up
- Configuration: crashes — Crash retention settings