Skip to content

Claude Code and agents ​

berth was built for machines where many Claude Code sessions run at once. Three layers make a session right by default; none of them requires a human to type a port.

Install the plugin ​

bash
claude plugin marketplace add amiable-dev/berth
claude plugin install berth@berth

The marketplace entry points at the npm package, so the plugin carries the compiled CLI. Update with claude plugin update berth@berth. The manual route (berth hooks install, claude mcp add --scope user berth -- berth mcp, the rules text below) still works for people who prefer a global install; berth doctor tells you which is active and warns if both are, because that runs the SessionStart hook twice.

Rules for your global CLAUDE.md ​

berth doctor warns when your global instructions have no Ports section. Paste this one in; it is what tells an agent to decode a port rather than guess one, and it is the rule that makes an advisory registry work at all.

markdown
## Ports (berth)

Every project on this machine has a permanent number P in `~/.config/berth/policy.toml`, and its ports are `10000 + 1000·P + 100·W + R` (W = worktree, 0 for the main checkout; R = role: 00 web, 01 api, 02 db, 03 cache, 04 smtp, 05 mail-ui, 06 docs, 07 worker, 08 otlp-grpc, 09 otlp-http, 10–99 project-named extras). Reading a port is decoding it: subtract the base first, then read P, W, R off what is left. 13204 − 10000 = 3204, so project 3, worktree 2, smtp; 31010 − 10000 = 21010, so project 21, worktree 0, role 10. Do not read the digits in place — that only works while P is a single digit.

- At session start berth injects this project's block and exports `PORT`, `API_PORT`, `DB_PORT` and the other role ports. If they are missing, run `berth env --shell` (or `berth env --dotenv`, `berth env --compose-override`).
- Pass the port explicitly (`--port $PORT`, `-p $PORT`) and use strict mode (`vite --strictPort`). Never let a framework pick a port. If the port is taken, stop and report; do not increment.
- Before binding anything outside your exports, ask who holds it: `berth who <port>`. New service: `berth claim --role <role>` or `berth claim --extra <name>`. Scratch server: `berth claim --dynamic 1` (40000–41999, 8 h TTL). Release what you no longer need: `berth release --port <port>`.
- Do not kill a listener you do not own. `berth free <port>` refuses other sessions' ports; report and ask instead.
- The ports under `[shared.*]` in your policy are one stack owned by one project. Do not start another; point at it (`BERTH_SHARED_*` exports).
- 5000 and 7000 belong to macOS (AirPlay).
- Tell the human the exact URL you bound. `berth check` shows what needs attention; it never blocks or kills.

Reserved ranges beyond AirPlay come from your own policy's [reserved] table, so add a line for anything else this machine keeps clear.

What a session gets ​

Hooks inform. At session start (startup, resume, clear, compact) the plugin runs berth context. It is read-only against allocation state, races a hard 180 ms deadline and always exits 0. It injects a "Ports (berth)" section, headed with the plugin's version, scoped to this project:

## Ports (berth 0.1.11)
Project cith-watch (P=4) owns 14000–14999. This checkout is W0 (main) → 14000–14099.
Ports: web 14000 · api 14001 · db 14002 · cache 14003 · smtp 14004 · mail-ui 14005 · docs 14006 · worker 14007 · otlp-grpc 14008 · otlp-http 14009.
Exported for this session: PORT (web) and WEB_PORT, API_PORT, DB_PORT, …; BERTH_BLOCK=14000-14099.
Legacy ports this repo still hardcodes: 5173, 8787, 5439, 6380 (migrate with `berth env --compose-override` or `berth env --dotenv`).
No conflicts touching this project in the last check.
Shared observability (owned by skills-telemetry; do not start another): grafana 3000 · prometheus 9090 · …
Rules: pass the port explicitly (--port $PORT, vite --strictPort); never let a framework pick one. …

and appends the role-port exports, BERTH_* variables and the plugin's bin/ directory to the session's environment, so berth and $PORT work in every Bash call. At session end, berth session-end records that the session ended; its unbound leases show as stale afterwards.

Skills teach. Two skills load on demand:

  • berth-ports triggers before a dev server, database or Compose stack is started, on "address already in use", or when asked which port a service uses. It walks the agent through berth env, the strict-port flag per framework, berth claim, berth who, and the nine states.
  • berth-onboard triggers on "register this repo in berth" and similar. It runs berth doctor, berth init when there is no policy, berth project add ., then env, the Compose override and launch-json --write, and reports the block and ports.

MCP executes. The plugin registers berth mcp, a stdio server with six tools: berth_check, berth_who, berth_ls, berth_claim, berth_release, berth_env. Clients that prefer tools to a shell use these; for Claude Code, which has Bash, the CLI's --json output is the primary interface.

The agent flow, end to end ​

  1. You install the plugin once (or ask the agent to).
  2. In a repository the agent has not seen, you say "register this repo in berth". The skill registers it with the next free permanent number and records its existing ports.
  3. From then on, every session in that repository starts with its ports in context and in the environment. The agent binds servers on them, claims what it starts, and reports the URL it actually bound.
  4. When something collides, the agent asks berth who instead of incrementing or killing, reports the holder, and follows the advisory.

What an agent may do ​

Everything self-scoped: learn its ports, claim and release its own leases, adopt a server it started, register the repository it is in, write that repository's launch.json, list worktrees, sync names, run doctor and check.

Human-only: free, any command with --force, hooks install and hooks uninstall, worktrees prune, init --force, adopt --owner human, and changing an existing project's number. The CLI refuses these unless a human is at an interactive terminal with no Claude marker in the environment, or BERTH_ALLOW_DESTRUCTIVE=1 is set deliberately. Refusals and overrides are written to ~/.local/state/berth/audit.log. This is protection against accidental misuse, not a security boundary; see the safety model.

Other agents and scripts ​

Anything that can run a shell uses the same CLI, and --json is the contract on every read command. Servers started by a Claude session are attributed to it through the session marker in their environment; servers started elsewhere are attributed by working directory and container labels.

Released under the MIT licence.