berthdocs

Reference

Events and gates

Every event Berth announces, the gates that can refuse actions, and the variables hooks get.

The envelope

{ "type": "worktree.created", "time": "2026-10-03T09:12:44Z", "box": "devl", "origin": "app",
  "data": { "location": "cal", "name": "billing", "path": "/home/me/work/cal-billing", "branch": "billing" } }
FieldMeaning
typeWhat happened, area.thing
timeWhen
boxThe box it happened on, or a laptop event's box (box.connected, forward.*)
originThe tool it came from: app, berth (the default), claude, detected, flow:<id>, guard, phone, a hook's tool…
errorFor failures
dataDepends on the type; below

Box events reach the laptop too: the laptop agent relays every online box's events with box set. berth events and GET /v1/events stream them.

Anything can announce its own: berth emit TYPE key=value… on the laptop, berthd emit TYPE key=value… --origin TOOL on a box. A type is two words of lowercase letters, digits and dashes joined by a dot, each starting with a letter and at most 32 characters; an origin is lowercase letters, digits and dashes.

Box events

Agents and sessions

EventData
agent.readypath, agent and the tool's own id (below): a new agent at its prompt
agent.startedthe same: working on a prompt
agent.waitingthe same: needs you (a permission or a question). At a startup question Berth sends it itself, with path, agent, session and reason: "startup question"
agent.finishedthe same: done with its turn
task.createdlocation, name, path, branch, session, agent, from_session
session.startedname, location, path, command
session.stoppedname
session.sentname: a prompt was typed in (never the prompt)
session.openname, location, path, open, agent: asks the app to show a new session
exec.finishedlocation, path, command, exit_code
preview.openlocation, name, path, port, url_path: an agent asks the app to show a page (berthd preview)

Agent events come from the agents' own hooks (berthd integrations install), with the tool as their origin. Claude Code sends all four, with session_id; Codex sends agent.finished with turn_id, and Cursor agent.finished with conversation_id and status.

Locations and worktrees

EventData
location.addedlocation, path, url (when cloned)
location.removedlocation
worktree.createdlocation, name, path, branch (origin detected when another tool made it)
worktree.removedlocation, name, path
worktree.setup.started, .finished, .failedlocation, name, path, script, log
worktree.archive.started, .finished, .failedthe same
worktree.syncedlocation, name, mode, ok, base, conflicts
worktree.paused, worktree.resumedlocation, name, path
service.startedlocation, name, path, service, port
service.stoppedlocation, name, path, service
service.failedlocation, name, service, error

Automation, config and the box

EventData
flow.startedflow, scope, run, path
flow.finishedflow, scope, run, status, path
notifytitle, body, flow, path, location: a flow's or the guard's notification
guard.actedaction, location, name, path, services, sessions, memory_percent, reason
config.changedwhat changed: location, flows, env or guard
hooks.changed
kit.installedlocation, kit, version, source, warnings
kit.removedlocation, kit
skills.installed, skills.removedskills, agents, target, location, paths
secret.failedlocation, name, path, variable, ref, reason: a secret reference could not be read, so the variable was left unset; never the value
secret.resolvedlocation, name, path, variable, ref: one that failed reads again
share.started, share.stoppedid, port, url
unit.started, unit.stopped, unit.restartedname
phone.changedenabled
box.upgradedbuild

Flows triggered by a schedule or GitHub receive schedule.fired or github.review_comment, github.pr_review, github.check_failed, github.pr_merged as their event; these start the flow directly and are not announced to hooks.

A flow's scope is box or repo:<location>, whichever layer (committed, kit or the box's own) the flow comes from; only the flow that wins its id runs, so flow and scope name one flow.

Laptop events

EventData
laptop.started
box.connected
box.disconnected, box.untrustednone; the envelope's error says why
forward.started, forward.failed, forward.removedid, local, remote (and the envelope's error on a failure)
queue.changedqueued, failed
queue.deliveredid, box, session, attempts
queue.failedid, box, session, reason (never the prompt)
kit.addedkit, source
app.changedkey: an app settings document changed
hooks.changed

Gates

Gates are before: hooks on a box. They run before the action, in order (the box's hooks.json and plugins, then the repository's), and one that exits non-zero refuses it with what it printed. Gates in a laptop's hooks.json never run.

GateData
before:location.addlocation, path, url
before:worktree.createlocation, name, branch, base
before:worktree.removelocation, name, path
before:worktree.synclocation, name, mode
before:worktree.pause, before:worktree.resumelocation, name
before:task.createlocation, name, branch, base, agent, command
before:session.startname, location, path, command
before:session.stopname
before:session.sendname, and from: "phone" for the phone
before:execlocation, path, command
before:kit.installlocation, kit, source
before:kit.removelocation
before:skills.install, before:skills.uninstallskills, agents, target, location
before:config.changelocation, flows, env or guard: what is about to change
before:phone.change

A task runs before:task.create, then the worktree's and the session's own gates.

What a hook gets

VariableMeaning
BERTH_EVENTThe event's type
BERTH_EVENT_BOXIts box
BERTH_EVENT_ORIGINIts origin
BERTH_<FIELD>Each data field, upper-cased, with other characters as _: BERTH_PATH, BERTH_NAME, BERTH_LOCATION, BERTH_SESSION, BERTH_AGENT…
BERTH_ORIGINThe hook's tool, or hook: Berth commands the hook runs announce their events with it
BERTH_PLUGIN_DIRA plugin's hooks: its folder

The event is also on stdin as JSON. Hooks from a repository's config get the worktree's environment as well; see files and environment.

On this page