Logging
Three Bossanova services write structured logs: the bossd daemon, the boss
TUI, and the bosso server. Each one writes newline-delimited JSON to a rotated
file, and boss tail reads those files for you so you never have to remember
where they are.
Agent output — what Claude or Codex actually printed in a session — is a
separate surface, stored elsewhere in a different format, so you will not
find it in bossd.log. boss tail reads it too, but you have to ask for it by
agent-session id. See
Agent and chat logs below.
For run-level timing and cost telemetry, use boss cost. boss tail answers
"what did this process log?"; boss cost answers "how long did runs take, how
many model/tool calls happened, and how much direct subagent time was running?".
The boss tail command
With no arguments, boss tail prints the last 10 lines of the daemon log:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail
No equivalent — this command runs locally
Output is one line per record, with the time, the source service, the level, and the message:
14:02:11 bossd info session created repo=9f1c4a7b2e6d0358
14:02:12 bossd info worktree ready repo=9f1c4a7b2e6d0358
14:02:19 bossd error plugin dispatch failed plugin=repair
Sources
boss tail takes any number of positional sources. Three name a service log,
and only these three carry service logs:
| Source | Contents |
|---|---|
bossd | The daemon: session lifecycle, git operations, plugin dispatch. Default. |
boss | Every boss command except boss tail itself, TUI included. It logs to file only, so this is the only place its records appear. |
bosso | The server backing the web UI. |
Anything else is read as an agent-session id, which selects one agent log — see Agent logs below.
Name a source explicitly to choose which one you read:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail bossd
No equivalent — this command runs locally
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail boss
No equivalent — this command runs locally
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail bosso
No equivalent — this command runs locally
boss tail always reads the log files on the machine you run it from. It is not
a daemon call, so the global --remote and --host flags do not redirect it —
to read another machine's logs, run boss tail over there.
A source that is neither a service nor a plausible agent-session id is rejected rather than silently treated as a filter:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail daemon
No equivalent — this command runs locally
boss: unknown log source "daemon" (want one of: bossd, boss, bosso, or an agent-session id)
--all merges every service
--all reads all three logs and interleaves them by timestamp. Records that
share a timestamp are ordered bossd, then boss, then bosso:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail --all -n 50
No equivalent — this command runs locally
A source and --all are mutually exclusive — --all already includes every
source, so asking for both is a mistake rather than a narrowing:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail bossd --all
No equivalent — this command runs locally
boss: pass a source or --all, not both
Note that -n applies per source, so boss tail --all -n 50 reads up to 50
lines from each of the three logs, not 50 lines in total.
Flags
| Flag | Default | Meaning |
|---|---|---|
--all | false | Merge every service log. |
-n, --lines | 10 | Physical lines to read per source before filtering. |
-f, --follow | false | Keep reading as the log grows. |
--repo | (none) | Only records for this repo. |
--plugin | (none) | Only records from this plugin. |
--level | (none) | Only records at this level. |
--json | false | Emit one parseable JSON object per line. |
Following with --follow
-f / --follow prints the backlog first, then streams new records as they are
written. It keeps running until you interrupt it:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail -f
No equivalent — this command runs locally
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail --all --follow
No equivalent — this command runs locally
Following works across a rotation: when a log rolls over, boss tail picks up
the replacement file without duplicating or dropping records. Piping into a
command that closes its input early is a clean exit, not an error:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail -f | head -20
No equivalent — this command runs locally
Two behaviours worth knowing
Both of these are in the command's own --help text, because both quietly
mislead if you assume otherwise.
-n counts physical lines read from each source before filtering. It is
not "show me 10 matches". boss tail -n 10 --level error reads the last 10
lines and then discards the ones that are not errors, so it can legitimately
print nothing at all even though the log is full of errors further back. When
you are filtering, raise -n generously:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail -n 2000 --level error
No equivalent — this command runs locally
Non-JSON lines always pass filters. Anything that reaches a log file without being structured JSON is not parseable, so no filter can decide whether it matches — and hiding it would be exactly the wrong outcome. Those lines are always shown verbatim, marked with the source service:
bossd | unstructured line appended to the log
That is a safety net rather than an everyday sight, because the logger itself
only ever writes JSON to these files. A crash in particular does not land
here: a Go panic goes straight to the process's stderr without passing through
the logger, so boss tail cannot show it. Under the macOS launch agent that
boss daemon install writes, that stderr is
~/Library/Logs/bossanova/bossd.stderr.log; under the Linux systemd user unit
it goes to the journal (journalctl --user -u bossd.service).
Where the logs live and how they rotate
boss tail exists so you do not need these paths, but they are useful for a bug
report or a grep across a long history.
The log directory is $XDG_STATE_HOME/bossanova/logs if XDG_STATE_HOME is
set, and ~/.local/state/bossanova/logs otherwise (on both macOS and Linux —
XDG_STATE_HOME is normally unset on macOS, so the fallback is what you have).
Each service writes <service>.log there:
ls ~/.local/state/bossanova/logs
# boss.log bossd.log
bosso.log appears there only if you run the server yourself; the hosted one
logs on its own machine. A log file that does not exist is treated as an empty
log rather than an error, so boss tail bosso simply prints nothing and
--all still works with only two of the three files present.
Rotation is size-based. A log rotates when it reaches 5 MB, and one
backup is kept, uncompressed — so each service retains roughly 10 MB, and the
three services together are bounded at about 30 MB. Backups are written beside
the current file with a timestamp in the name, matching <service>-*.log:
ls ~/.local/state/bossanova/logs
# boss.log bossd-2026-08-08T09-14-52.301.log bossd.log
You do not need to name those backups yourself. When the current file holds
fewer lines than -n asks for, boss tail reads back into the rotated backups
transparently, so boss tail -n 5000 spans the rotation boundary.
Because retention is bounded and rotation is by size, a chatty period can push older records out within minutes. If you need to keep something, redirect it while it is still there:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail --all -n 20000 --json > /tmp/bossanova-logs.ndjson
No equivalent — this command runs locally
Agent and chat logs are a different surface
bossd, boss and bosso carry service logs only. They record what
Bossanova did, never what the coding agent printed — so a service log that looks
silent is not evidence that the agent is idle, only that the data lives in a
different file. Agent output is captured separately:
- The format depends on how the chat runs. An interactive chat is a raw
terminal capture of the agent's tmux pane, mirrored with
tmux pipe-pane: not JSON, no per-line timestamps, and full of terminal escape sequences. A headless run instead writes one JSON object per output line,{"ts": "…", "text": "…"}, covering the agent's stdout and stderr, with occasional[runner]diagnostics from the agent runner interleaved in the same shape. The agent's own output is the opaque string intext— for Claude that is a line of--output-format stream-json, so reading it means unwrapping twice. Neither format is the structured service record thatboss tailrenders. - There is one file per chat, named after the chat's agent-session UUID:
<agent-session-id>.log. The same directory also holdsrepair-<session-id>.logfor the repair plugin's own runs — note that those are keyed by session id, not agent-session id. - The files live in an
agent-logsdirectory next to your worktree base directory — the sibling ofworktree_base_dirfrom your settings. The full path form is<worktree_base_dir>/../agent-logs/<agent-session-id>.log; with the defaultworktree_base_dirof~/.bossanova/worktrees, that directory is~/.bossanova/agent-logs. boss tailreads them too, by agent-session id — see Agent logs below.
So: to see why a session failed to start, read bossd.log via boss tail. To
see what the agent said, read the chat — attach to the session, or read the
transcript — or point boss tail at the agent-session id, which is the quickest
route for a headless run that is still in flight.
To inspect the run as telemetry instead of text, use boss cost <session-id>.
That command reads the daemon's agent-run ledger and reports wall-clock,
parent-only time, parallelism, model-call counts, tool-call counts, and token
counts when the agent runner provided them.
Attaching to a chat whose headless run is still going is refused, and the refusal prints the exact agent-log path to follow instead.
Agent logs
Pass an agent-session id where you would pass a service name:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail 3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607 -f
No equivalent — this command runs locally
boss tail detects which of the two formats above the file holds, per file,
from its first non-empty line — the caller does not have to know whether the
chat ran interactively or headless. JSON lines are unwrapped to their text,
raw captures have their terminal escape sequences stripped, and either way the
result prints as an unstructured line, so a malformed entry is shown verbatim
rather than dropped.
Because agent output carries no level, repo or plugin, it is never removed by
--level, --repo or --plugin — the same rule that keeps raw diagnostics
visible. Filtering a mixed tail narrows the service records only.
Naming several sources interleaves them by timestamp, which is the point when you want to see a daemon event next to what the agent was doing at the time:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail bossd 3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607
No equivalent — this command runs locally
That raises the question of what timestamp an interactive capture has, since it has none of its own. The rule: an untimestamped line inherits the last timestamp seen in its own file, and a line with nothing before it inherits the log file's modification time. A raw line therefore sits next to the output it followed. It is never left at the zero time, which would sort the whole capture to the Unix epoch and float it ahead of every other source.
--all is unaffected: it still means "every service log". The agent-logs
directory holds one file per agent session ever run, so merging all of them is
not a useful default. Name the sessions you want instead.
An agent-session id whose log does not exist yet is still a valid source under
-f — the follower waits for the agent to create it. If the agent-logs
directory itself is absent, which is simply a Bossanova that has never run an
agent, boss tail says so and exits successfully rather than failing:
no agent logs: /Users/you/.bossanova/agent-logs does not exist
JSON output and filtering
Filters
--level, --repo, and --plugin each restrict the output to records whose
matching field equals the value you pass. Matching is case-insensitive, and
--repo matches either a repo or a repo_id field.
repo_id is always a repository ID, but the repo field is written by several
subsystems and may hold the repository ID, its local path, its display name, or
— from bosso — its origin URL. The rendered line shows repo=… for the repo
field only, so a record carrying just repo_id still matches --repo but
prints no repo token at all. Use --json to see which of the two a given record
actually carries before settling on a filter value.
Plugins log through the daemon's plugin host, which wraps the plugin's own JSON
record inside the outer daemon record. boss tail unwraps that before
filtering and rendering, so --plugin, --level, and --repo match the
plugin's inner fields as well as the daemon's outer ones, and the rendered
message is the plugin's message rather than an escaped blob.
Remember that filters are applied after -n has read its lines. Every filter
example below raises -n for that reason.
--json
--json emits exactly one JSON object per line, suitable for jq. The record
is the source record plus a few _boss_-prefixed keys:
| Key | Meaning |
|---|---|
_boss_service | The source log the record came from (bossd, boss, or bosso). Always present. |
_boss_plugin | The unwrapped inner plugin record, when the line came from a plugin. |
_boss_raw | true for a line that was not valid JSON; the text is in line. |
For a plugin record the top-level level and message are also replaced
with the plugin's own values, so --json reports what the rendered line shows
rather than the daemon's outer wrapper text. Every other key is the source
record's own.
Recipes
Every error-level record, across all three services. --level is an exact
match, so this selects error and not warn or fatal. Read a deep backlog so
the filter has something to select from:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail --all -n 5000 --level error
No equivalent — this command runs locally
Everything one repository did. Useful when several sessions are running and the daemon log is interleaved:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail -n 5000 --repo 9f1c4a7b2e6d0358
No equivalent — this command runs locally
Watch one plugin live. Prints the recent backlog for that plugin, then streams its new records:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail --follow -n 200 --plugin repair
No equivalent — this command runs locally
Count errors by plugin. --json plus jq handles anything the built-in
filters do not:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail -n 20000 --json | jq -r 'select(.level == "error") | .plugin // "core"' | sort | uniq -c | sort -rn
No equivalent — this command runs locally
Follow everything, merged. The closest equivalent of watching all three logs at once, ordered by timestamp:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail --all --follow -n 100
No equivalent — this command runs locally
Extract just the message text from a merged JSON stream, tagged with the service it came from:
- Chat
- CLI
- MCP
No equivalent — this command runs locally
boss tail --all -n 500 --json | jq -r '"\(._boss_service)\t\(.message // .line)"'
No equivalent — this command runs locally
See also
- Troubleshooting — the runbook that these commands support.
- Privacy — what a bug report includes from these logs, and what those logs can contain.