portlock
v0.1.1
Published
Deterministic runtime identity for multi-agent git worktrees
Maintainers
Readme
Portlock
Portlock gives each git worktree a deterministic local runtime identity.
Instead of manually juggling ports across branches, Portlock assigns a stable base range to each worktree and derives the rest of your local topology from it:
- service ports
- service URLs
- namespace values
- machine-readable metadata
This is useful when multiple developers, agents, or parallel worktrees are running the same app on one machine.
Why It Exists
When several worktrees share one laptop, the usual local defaults break down:
- two services try to bind the same port
- one worktree talks to another worktree's API by accident
- local Redis keys, database names, and temporary state blur together
- scripts assume
localhost:3000means "mine" when it does not
Portlock treats each worktree like a lightweight local tenant.
Install
npm install -g portlockOr run it without a global install:
npx portlock initFor local development on this repo:
npm linkQuick Start
- Create a
.portlock.jsonfile in your repo root. - Run
portlock initinside a git worktree. - Source
.env.portlockfrom your local dev scripts. - Use
.portlock/meta.jsonif another tool needs structured metadata.
Example:
portlock init
source .env.portlockExample output:
API_PORT=3100
WEB_PORT=3101
NEXT_PUBLIC_API_URL=http://127.0.0.1:3100
PORTLOCK_CLAIM=feature-x
PORTLOCK_BASE=3100
PORTLOCK_LABEL="FEATURE-X / 3100"Example Config
Create .portlock.json in your repo root:
{
"basePort": 3000,
"step": 100,
"stripPrefixes": ["users/john/", "branches/"],
"services": {
"api": {
"offset": 0,
"env": {
"API_PORT": "{port}",
"API_HOST": "0.0.0.0",
"API_ORIGIN": "http://127.0.0.1:{port}"
}
},
"web": {
"offset": 1,
"env": {
"WEB_PORT": "{port}",
"WEB_ORIGIN": "http://127.0.0.1:{port}",
"NEXT_PUBLIC_API_URL": "http://127.0.0.1:{api}"
}
},
"workers": {
"offset": 80,
"env": {
"WORKERS_HEALTH_CHECK_PORT": "{port}"
}
}
},
"namespace": {
"env": {
"PORTLOCK_NAMESPACE": "app:{claim}",
"REDIS_KEY_PREFIX": "app:{claim}",
"PGDATABASE": "app_{claim}"
}
},
"metadata": {
"env": {
"PORTLOCK_CLAIM": "{claim}",
"PORTLOCK_BASE": "{base}",
"PORTLOCK_LABEL": "{label}"
}
}
}Template tokens:
{base}: assigned base port{claim}: derived worktree claim{label}: human-readable label{port}: current service port{api},{web},{workers}: sibling service ports
Commands
portlock init
portlock env
portlock status
portlock release
portlock cleanup
portlock current --json
portlock resolve apiWhat they do:
init: claim or reuse a base range for the current worktree and write local outputsenv: print derived environment variables for the current worktreestatus: list active claims on the current machinerelease: release the current worktree's claimcleanup: remove stale claims for deleted worktreescurrent --json: print the current worktree's identity as JSONresolve <service>: print the resolved port or origin for a service
Generated Files
portlock init writes:
.env.portlock.portlock/meta.json
.env.portlock is meant for shell scripts and local process startup.
.portlock/meta.json is meant for tools that want structured data without parsing shell syntax.
How It Works
Each worktree discovers its own runtime identity locally.
When you run portlock init, Portlock:
- reads the current git context
- derives a claim from the branch and worktree
- acquires a machine-global lock
- checks the shared claim store at
~/.portlock/lock.json - reuses or assigns a base range
- writes local outputs into the current worktree
Example:
/Users/john/code/app branch=main -> base 3000
/Users/john/code/app-feature-a branch=feature-a -> base 3100
/Users/john/code/app-feature-b branch=feature-b -> base 3200Different branches do not need to know about each other directly. They coordinate through the shared machine-level claim store during init.
Integration Example
In a shell script:
if [ -f .env.portlock ]; then
set -a
source .env.portlock
set +a
fi
API_PORT="${API_PORT:-3000}"
WEB_PORT="${WEB_PORT:-3001}"Scope
Portlock does:
- deterministic local port assignment
- cross-service URL derivation
- optional namespace generation for local shared state
- machine-readable worktree metadata
Portlock does not:
- manage processes
- provision databases or Redis instances
- create proxy hostnames
- isolate worktrees with containers or network namespaces
Notes
- Port assignment is machine-global, not repo-local
- Claims are tracked with repo metadata for cleanup and display
- Duplicate claim suffixes are stable only for the lifetime of an active claim
Development
npm test
npm pack --dry-runGitHub Actions workflows are included for CI and publishing.
License
MIT
