You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two worktrees of the same project both run pnpm dev, and both want 5173. The
second one loses. The setup script that just installed the dependencies has no
way to know it should have picked another number, because nothing tells it which
worktree it is running in.
Emdash treats the port as part of the worktree lifecycle: .emdash.json declares
setup/run/teardown and the runner injects $EMDASH_PORT, a per-task port, so a
script reads PORT=$EMDASH_PORT pnpm run dev. It is the one thing in the
category that handles the shared-port problem cleanly.
lich already has both halves of the seam:
a per-project worktree setup script (worktree.setup, internal/store/settings.go:130), run when the worktree is created;
per-session environment injection (Service.sessionEnv, internal/terminal/terminal.go:213) — where LICH_PORT, LICH_TOKEN and LICH_SESSION_ID already come from.
Shape
Derive a stable port per worktree — hashed from the checkout path into a
range — so the number survives a restart and the same worktree always gets the
same one.
Export it to the setup script and to every session spawned in that worktree.
NotLICH_PORT: that name is the hook listener (docs/hooks/README.md). LICH_WORKTREE_PORT.
Open
Range, and what happens on collision: probe for a free port when assigning, or
trust the hash and let the script fail loudly?
Does the main checkout get one too, or only worktrees?
Range configurable per project, or fixed?
Windows: the setup script already skips its sh -c wrap there, but the
variable can still be injected.
Related
Prerequisite for a preview surface in the dock — without a known port there is
nothing to point it at.
Shipped in #126 (0192085). Recording the answers to the open questions here, since the issue was needs-shaping and the decisions otherwise only exist in the commit.
Range, and what happens on collision — pure hash, no probing. FNV-1a of the cleaned checkout path folded into a fixed 1000-port window (internal/terminal/worktreeport.go). Nothing probes and nothing binds, so two checkouts can land on the same number and only the dev server started second finds out. Probing was the alternative and it costs exactly the stability that is the point: the previous server still holding the port would move the number on every restart. Recorded as a Known Ceiling.
Does the main checkout get one too — yes. It is half of the original collision, and deriving from the session's start directory treats it like any worktree with no extra code.
Range configurable per project — fixed, not configurable. 24000–24999: below the ephemeral range Linux allocates outbound connections from (32768), clear of the ports frameworks default to (3000, 5173, 8080), and clear of lich's own pinned listener (47821).
Windows — the variable is injected normally; only the setup script's sh -c wrap is still skipped there.
One thing worth knowing that the issue did not ask: the port derives from the session's start directory, not the repository root. In practice they are the same — sessions start at the worktree root — but a session started in a subdirectory would get a different number.
The dock preview surface this unblocks is not covered here and has no issue yet.
Activity
omartelo commentedon Jul 29, 2026
Shipped in #126 (
0192085). Recording the answers to the open questions here, since the issue wasneeds-shapingand the decisions otherwise only exist in the commit.Range, and what happens on collision — pure hash, no probing. FNV-1a of the cleaned checkout path folded into a fixed 1000-port window (
internal/terminal/worktreeport.go). Nothing probes and nothing binds, so two checkouts can land on the same number and only the dev server started second finds out. Probing was the alternative and it costs exactly the stability that is the point: the previous server still holding the port would move the number on every restart. Recorded as a Known Ceiling.Does the main checkout get one too — yes. It is half of the original collision, and deriving from the session's start directory treats it like any worktree with no extra code.
Range configurable per project — fixed, not configurable. 24000–24999: below the ephemeral range Linux allocates outbound connections from (32768), clear of the ports frameworks default to (3000, 5173, 8080), and clear of lich's own pinned listener (47821).
Windows — the variable is injected normally; only the setup script's
sh -cwrap is still skipped there.One thing worth knowing that the issue did not ask: the port derives from the session's start directory, not the repository root. In practice they are the same — sessions start at the worktree root — but a session started in a subdirectory would get a different number.
The dock preview surface this unblocks is not covered here and has no issue yet.
Generated by Claude Code