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.


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:

  1. Ensure your SSH key is added to ssh-agent: ssh-add ~/.ssh/id_rsa
  2. Verify you can connect without prompts: ssh hostname
  3. Check ~/.ssh/config for 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

  1. Verify watch_binary path matches the actual binary location
  2. Check the watch.debounce setting isn’t too high
  3. Ensure watching: true (the default) isn’t set to false

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:

  1. Load your configuration
  2. Create tmux sessions for each worktree
  3. Start your services
  4. Begin watching for binary changes
  5. 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:

  1. You run go build -o ./bin/backend ./cmd/backend
  2. Trellis detects ./bin/backend changed
  3. The backend service is gracefully restarted
  4. 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

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:

  1. On Trellis launch: All enabled services (enabled: true, the default) start immediately
  2. On worktree activation: When you switch worktrees, all services stop and restart in the new worktree’s context
  3. 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:

  1. You recompile: go build -o ./bin/backend ./cmd/backend
  2. Trellis detects the binary changed
  3. Debounce timer starts (waits for additional changes)
  4. After debounce period, the service is gracefully stopped (SIGTERM)
  5. The service restarts
  6. A binary.changed event 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:

  1. Stops services in the current worktree
  2. Switches to the new worktree
  3. 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:

  1. Work on multiple features simultaneously - Each worktree has its own services running its own binaries

  2. Quick bug fixes - Create a worktree from main, fix the bug, deploy, delete the worktree

  3. Code review - Check out a PR in a worktree, run the services, test it

  4. 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 in live mode.

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.

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.

The navigation picker open over the terminal page

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.

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 level
  • msg:~"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.

A log viewer streaming a remote error log over SSH, showing a panic entry

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

The entry details panel for a log line, with per-field copy buttons

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.

A Claude session that traced a production crash and wrote the fix

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, …).

A worktree home page with its Claude sessions, terminals, and case

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:

  1. 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).
  2. 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.
  3. 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.jsonmodel) 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:

  1. Select an existing case or create a new one
  2. Optionally provide a transcript title
  3. 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:

  1. 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).
  2. Lists changed files as checkboxes (all checked by default). Paths inside the live cases directory are rejected — only your selected files are staged.
  3. 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.

The Wrap Up modal: changed files, a linked trace, component chips, and a generated commit message

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. A claude.session.moved event is emitted.
  • Plans — Plan history is stored per session in .trellis/claude/plans/<session-id>.json and exposed at GET/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 a CommitEntry to case.json with the SHA, date, message, generated description, and files changed. The draft message comes from claude -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 -p synchronously (with a timeout) so it lands in the same commit, moves the case directory from cases/ to cases-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-side commitToCase orchestrator are agent-agnostic; the only per-agent differences are the Save-to-Case button label and the transcript directory.

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

The pair banner on an implementer session mid-loop

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:

  1. The implementer implements one phase.
  2. An inner paired review loop reviews that phase until the reviewer approves it (LGTM).
  3. The run sends the advance prompt (“implement the next phase…”) and the cycle repeats.
  4. 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.

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

The session inbox: stalled, errored, and waiting sessions on top, running sessions with live activity below

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 CLAUDE or CODEX agent 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 modelstate is a coarse two-value field derived per agent: Claude is running while generating with no pending control request, Codex while generating with no pending approvals; otherwise needs_you. state drives sorting and transition detection — it flips only on a real running ↔ needs_you change. A finer reason field (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 coarse running ↔ needs_you transitions, carrying {session_id, agent, worktree, display_name, state, reason, unread, trashed}) and session.activity (a lighter stream carrying {session_id, activity}, fired at tool/step boundaries — not per token — and only when the description changes).
  • NavigationGET /api/v1/inbox/ws?role=inbox|main is a single WebSocket endpoint serving two roles: the popup connects as role=inbox; every regular Trellis page connects as role=main (via inbox_main_ws.js in 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 new trellis-main window directly.
  • Initial listGET /api/v1/inbox/sessions returns the merged list of SessionRow{id, agent, worktree, display_name, state, reason, activity, unread, last_state_change_at} entries.
  • Hides — stored in localStorage with the state they were hidden in; a hide is discarded as soon as the recorded state no longer matches the live one.
  • 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 id directory 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}

An archived case: generated summary, components, and the wrap-up commit

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

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

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.

The usage page: daily cost and token totals across Claude Code and Codex

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.

The worktrees home page

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:

  1. All running services are stopped
  2. Trellis reconfigures for the new worktree’s directory
  3. Services are restarted in the new worktree
  4. Any configured pre_activate hooks 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:

  1. Enter a branch name (e.g., feature-x, bugfix-123)
  2. Trellis creates:
    • A new git branch from your default branch
    • A new worktree directory at ../<project>-<branch>
  3. 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.

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.

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:

  1. Trace ID — Enter the pattern to search for (e.g., req-abc123, a UUID, or any grep pattern)
  2. Trace Group — Select which group of log viewers to search
  3. Time Range — Specify when to search:
    • Range mode: Enter start and end times (e.g., 1h to now, or 6:00am to 7:00am)
    • Day mode: Select a specific date to search that entire day
  4. Report Name — Optional custom name (auto-generated if empty)
  5. Click Execute

The trace page: execute form and saved trace reports

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 a logging_defaults.timezone default) 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

A trace report correlating a request across the web, api, and errord services

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:text to 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.

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:

  1. See the trace ID in the crash list
  2. 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.

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.

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:

  1. Path specified with -config flag
  2. trellis.hjson in current directory
  3. trellis.json in 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):

  1. TRELLIS_API environment variable — set automatically in Trellis-managed tmux sessions, so commands run inside Trellis terminals always reach the right instance.
  2. -config <path> flag or TRELLIS_CONFIG environment variable — load the specified trellis.hjson and use its server.public_url if set; otherwise build the URL from server.host, server.port, and the configured TLS mode (server.tls_tailscale or server.tls_cert / server.tls_key).
  3. trellis.hjson (or trellis.json) auto-discovered by walking up from the current directory, resolved the same way.
  4. http://localhost:1234 fallback 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.

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.

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