Guides
Hooks and gates
Run your commands when something happens, or refuse an action before it does.
Hooks run your commands when something happens in Berth. They run on the
machine whose ~/.berth/hooks.json names them: hooks on a box run there,
next to its repositories; hooks on the laptop run on the laptop, where they
can notify you, open things, or call other tools.
{
"hooks": [
{ "on": "agent.waiting", "run": "osascript -e 'display notification \"$BERTH_PATH\" with title \"An agent needs you\"'" },
{ "on": "worktree.created", "run": "cd \"$BERTH_PATH\" && pnpm install", "timeout": "10m" },
{ "on": "before:worktree.create", "run": "case \"$BERTH_NAME\" in wip-*) echo 'no wip- worktrees'; exit 1;; esac" }
]
}The file is read again for every event, so edits apply immediately. The app edits it too: Automations → Advanced: shell hooks has a table per machine, templates to start from, and a live feed of events with New hook for this event.
Two kinds
- After the fact:
onis an event type (agent.finished), a prefix (worktree.*), or*. These run one at a time, off the critical path. They cannot change what happened, and their failures are only logged. - Gates:
onisbefore:and an action (before:worktree.create,before:*). They run first, in order, with a 30 second default timeout. A gate that exits non-zero stops the action, and whatever it printed is the error the app, the CLI or the agent sees. Gates run on the box; abefore:hook in the laptop's file never runs.
Where hooks live
| Where | Runs | For |
|---|---|---|
~/.berth/hooks.json on the laptop | On the laptop | Laptop events and every online box's events, relayed |
~/.berth/hooks.json on a box | On the box | That box's events and gates |
hooks in a repository's .berth/config.json | On the box, in the worktree | Only that repository's events and gates, with the worktree's environment |
A plugin's berth-plugin.json | Where the plugin is installed, in its folder | As above, while the plugin is on |
What a hook gets
Each hook runs through /bin/sh -c and gets:
- The event as JSON on stdin:
{"type":"worktree.created","time":"…","box":"devl","origin":"app","data":{…}} - The same as environment variables:
BERTH_EVENT,BERTH_EVENT_BOX,BERTH_EVENT_ORIGIN, and every data field upper-cased, asBERTH_PATH,BERTH_NAME,BERTH_LOCATION,BERTH_BRANCH, … BERTH_ORIGIN, which Berth commands the hook runs pass on, so their own events are attributed to the hook rather than to you.- Hooks from a repository's config also get the worktree's environment
(its ports and
BERTH_*variables, and the project'senv); plugin hooks getBERTH_PLUGIN_DIR.
Fields
| Field | Meaning |
|---|---|
on | An event type, a prefix like worktree.*, *, or before: and an action |
run | The command |
timeout | A Go duration: default 1m, or 30s for gates |
tool | An integration's name. The hook never runs for events whose origin is that tool, which stops two integrations from bouncing events back and forth. |
Events
The full list, with each event's data, is in events and gates. The ones you'll want first:
| Event | When |
|---|---|
agent.waiting | An agent needs you: a permission, a question |
agent.finished | An agent is done with its turn |
worktree.created, worktree.removed | A worktree appeared or went |
worktree.setup.failed | Its setup script failed |
task.created | A worktree with an agent in it |
box.connected, box.disconnected | Laptop only |
Box events reach the laptop too: the laptop agent relays every online box's
events, with box set, so a laptop hook on agent.waiting hears about every
agent on every box.
Agent events come from the agents' own hooks. berthd integrations install claude (or codex, cursor, all) on a box sets them up; the same command
with berth does it on a laptop.
Anything can announce its own events:
berth emit deploy.finished url=https://… # on the laptop
berthd emit deploy.finished url=https://… # on a boxGates
| Gate | Data |
|---|---|
before:location.add | location, path, url |
before:worktree.create | location, name, branch, base |
before:worktree.remove | location, name, path |
before:task.create | location, name, branch, base, agent, command |
before:session.start | name, location, path, command |
before:session.send | name, and from: "phone" from the phone |
before:exec | location, path, command |
before:kit.install | location, kit, source |
before:kit.remove | location |
before:config.change | what changes: a location's config, flows, env or the guard |
There are a few more; see events and gates. A
task runs before:task.create, then the worktree's and the session's own
gates.
Testing a hook
berth events # watch what arrives, live
berthd emit worktree.created path=/tmp/x name=x # fire one on a boxA gate's output is what people see when it refuses, so make it a sentence.
For anything with more than one step (run the tests, send failures back to the agent, notify you), use a flow instead.