Skip to content

Audit log

ShellWatch records every session that opens or closes and every signing-request outcome to an append-only audit log in the broker’s SQLite database. The Web UI exposes two views per account:

  • /audit/sessions — every session lifecycle event (open / close, source, who initiated, when).
  • /audit/signings — every PendingAction outcome (approved, denied, expired, cancelled) with the full request context preserved at decision time.

This page explains what’s recorded, what isn’t, and what guarantees the audit log gives you.

Written by session-lifecycle-writer.ts, which subscribes to TerminalManager open/close events.

  • Session ID, endpoint reference, source (ui / mcp / ssh).
  • Timestamps for open and close.
  • A CloseReason for closes (explicit close, transport error, idle timeout, account deletion, …). The TerminalManager.close() API requires a CloseReason so the writer always gets a meaningful value — there are no “unknown” closes.

Written by signing-requests-writer.ts, which subscribes to PendingActionStore resolutions.

Every PendingAction outcome is persisted with the full SignRequestContext (the same union the /sign/:id page renders from):

OutcomeWhen
approvedThe user (or another approver on the account) tapped their authenticator and /api/actions/:id/resolve accepted the assertion.
deniedThe user pressed Deny on /sign/:id.
expiredNobody resolved within the 60-second TTL.
cancelledThe requester (the SSH client / agent / MCP client) gave up before the user acted.

Per-trigger metadata is snapshotted at decision time — endpoint label, agent client hostname/OS/version, MCP client name/version, source IP. A subsequent rename does not rewrite history.

  • Session content. Session input and output are never persisted by the broker — the in-memory buffer that feeds attached viewers is a runtime structure only, never written to disk. Tamper-evident recording of what was typed and what scrolled by is not a ShellWatch responsibility; if you need it, record on the target host (e.g. script, tmux pipe-pane, auditd) where the bytes actually exist.
  • Failed authentications that never reached the PendingAction layer (e.g. a rejected bearer token on /mcp). Those live in the broker’s structured logs.
  • WebAuthn registrations / login ceremonies. They show up in /admin/general and the structured logs, but not in the audit tables.

Reads are account-scoped. There’s no global “see every account’s audit” view — admin is a role on the account, not a cross-tenant lens. A self-hosted single-account install is effectively a single audit stream; a multi-account install gives each account its own.

A determined operator with shell access to the broker host can of course read the SQLite file directly. The audit log is defense-in-depth, not a primary control against a compromised broker — see Security model.

The audit module never joins to live tables at read time. If you rename a passkey or relabel an endpoint, prior audit rows still display the names that were in effect when the decision happened.

In practice this means the endpoint “foo” you approved a session against last Tuesday is still labelled “foo” in the log even after it gets relabelled to “bar”.

/audit/sessions and /audit/signings use keyset pagination (cursor-based, not offset), so the read API is O(page size) regardless of how deep into history you scroll. Useful for self-hosted instances that accumulate a long tail.

Default page size is 50 rows; clients can override with ?limit= up to a server-side cap of 500. There’s no operator-tunable knob for the cap — if you need a different ceiling you’d recompile.

The audit tables live in the same SQLite database as the rest of the broker state. Back up data/shellwatch.db (and the WAL/SHM sidecars) like any other table. There is no built-in retention policy yet — append-only with no automatic pruning. If you need formal retention, snapshot and offload to your own pipeline.