fencepost
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@fencepostclaim a 30s lease on db:migrations, then write the migration file"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
fencepost
Leases with fencing tokens for agents that share one working tree. Windows-safe,
no daemon, and the core imports nothing outside node:*.
Several coding agents editing the same repository need mutual exclusion. The usual
answer is a lockfile holding a pid, and it fails in a way that is not fixable by
better stale detection: after a takeover, the evicted holder keeps everything it
needs to write and nothing is in a position to refuse it. fencepost gives every
claim a strictly increasing number and refuses any operation carrying a number that
is no longer current — so a holder that was declared dead, was written around, and
then woke up cannot land a write.
Library
import { createStore, acquire, check, release } from 'fencepost';
const store = createStore('.fencepost');
const migrations = { kind: 'lock', target: 'db:migrations' } as const;
const lease = acquire(store, migrations, { ttlMs: 30_000, owner: 'claude-code' });
try {
if (check(store, migrations, lease.token)) {
// the only party whose write is authorized
}
} finally {
release(store, lease);
}Every protocol step is an exclusive file creation. No rename, no replace, no
compare-and-swap, no coordinator — which is also why it behaves on Windows, where
renaming a directory another process holds a handle inside fails with EBUSY and
stays failed under real contention.
Requires Node 22.6+, which runs .ts directly, so working from a checkout needs no
build step and the core pulls in nothing outside node:*. What ships is compiled —
see Installing for why that distinction was load-bearing here.
Related MCP server: agentclaim
Command line
The CLI imports nothing but node:*, so an agent that can only shell out gets the
whole protocol.
fencepost claim --lock db:migrations --ttl 30000 # prints a token; exit 3 if held
fencepost check --lock db:migrations 17
fencepost run --lock db:migrations -- npx prisma migrate deploy
fencepost statusrun holds and renews for the child's entire lifetime and propagates its exit code.
A token from claim is kept only until its ttl, because the CLI process exits and
nothing renews it — that is the protocol working as designed rather than a gap: a
lease is only as alive as the process keeping it.
Exit codes: 0 success, 1 error, 3 held or stale token.
MCP server
{ "mcpServers": { "fencepost": { "command": "fencepost-server", "args": ["--root", "/path/to/project"] } } }Tools: claim, assert, renew, release, write, status.
Two things this earns over the library alone. write verifies the token, resolves
the target, and verifies again immediately before writing, so the check-then-write
window sits on the authority's side of the boundary instead of being hoped about.
And there is no daemon: each agent spawns its own server process, and
a real test proves they contend correctly through the filesystem
rather than through a shared in-process registry.
The MCP SDK is an optional peer dependency: only fencepost/server reaches it, and
the library and CLI import nothing outside node:*.
Installing
npm install fencepostnpm view fencepost versions shows a 0.0.0-stage entry ahead of the real releases.
That is a placeholder npm created when the first publish attempt went through
npm stage; dist-tags.latest points at the real version and nothing installs it
unless you ask for it by name. It is left alone deliberately: npm's rules for
unpublishing a package younger than a few days can take the whole package with it,
which is a bad trade for removing one line from a list.
Installs with zero transitive dependencies — the MCP SDK is an optional peer, so a
consumer that never imports fencepost/server never sees express, hono, cors or zod.
npm run verify:package proves the artifact rather than the source: it packs the
tarball, installs it into a throwaway directory offline, imports it from .mjs, runs
the installed CLI, and compiles a TypeScript consumer against the shipped .d.ts.
Getting it installable took two fixes that were invisible from inside the repository:
The entry point used to be
src/index.ts. Node refuses to strip types undernode_modules(ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so the package was unusable once installed while all 36 in-repo tests passed — they run against source, not against the artifact.npm run buildnow emitsdist/with declarations, usingrewriteRelativeImportExtensionsso the source can keep its explicit.tsspecifiers.The SDK was declared under
optionalDependencies, which npm installs by default: the consumer's tree came back with 94 packages (express, hono, cors, jose, zod). Optional peer dependencies withpeerDependenciesMeta.optionalis what actually keeps them out.
Both are asserted by verify-package.mjs, so the class of mistake is closed rather
than the two instances.
Measured cost
bench/RESULTS.md is generated by npm run bench on one Windows
box, and it does not flatter this library: a fenced acquire + release costs roughly an
order of magnitude more than proper-lockfile's lock + unlock, and one hot
resource serializes to a few dozen operations per second. The exact figures are left to
that file rather than repeated here — p50 on this machine drifts by tens of percent
between runs, and a number typed into prose is a number that silently goes stale.
The conclusion is stable even where the digits are not: this is a tool for claiming a migration or a port, not for guarding a per-file write path.
Durability is not one switch. A claim's flush is load-bearing — lose it and the next grantor can be handed the same generation number, giving two authorized holders — while the tombstone a release writes is deliberately unflushed, worth 1.8x on the round trip, because losing it only delays the next contender. See "Which fsyncs are load-bearing".
The benchmark also runs the scenario both libraries are actually distinguished by, as two real OS processes. A holder wedges its event loop past its own expiry, a second process takes over, and the wedged holder resumes:
proper-lockfile— the rival got in, the original holder'sonCompromisednever fired, and itsrelease()deleted the lockfile that belonged to the rival.fencepost— the successor got a higher generation, and the wedged holder's gate call returnedfalseand its release became a no-op.
That is not a knock on proper-lockfile, whose own README lists this exact cause; it
is the difference between being stopped at your next refresh and being stopped at the
write.
What is not guaranteed
Read docs/ before relying on any of it. In short:
Writes that bypass the gate are outside the guarantee. An agent editing with its own tool is not stopped by a fence it never consulted.
A hard-killed holder blocks the resource until its ttl expires. Nothing inspects the dead process — no heartbeat, no exit hook, no pid check — because liveness detection is the thing this design refuses to trust. ttl is a tuning knob, not a formality.
Hard links are two identities.
realpathdoes not unify them whilestatreports one(dev, ino)pair, so the true identity is available and unused. Closing it needs multi-key acquisition, which rewrites every signature inlease.ts; it is deferred on budget, not on knowledge, and a test pins the current behaviour so the docs must change along with the fix.Network shares are untested, and a clock that steps forwards can expire a lease early — the fence is what makes that survivable, not the clock logic.
Layout
path | what it holds |
| filesystem primitives, restricted to operations Windows promises |
| claim wire format, and the live/expiry rule |
| resource identity — the equality test the whole lock rests on |
| the protocol: acquire, renew, release, and the fence gate |
| command line entry point, no dependencies |
| MCP server, mediated writes, path confinement |
| contention, hard kill, clock steps, corruption, CLI exit codes, MCP round trip |
| installs the built tarball elsewhere and uses it as a stranger would |
| latency, contention throughput, the two-process wedged-holder comparison, generated results |
| three design notes: the guarantees, the identity evidence, the server |
Development
npm install
npm test # 39 tests, ~18s, runs against source (no build needed)
npm run typecheck
npm run build # emits dist/ (JS + .d.ts) from tsconfig.build.json
npm run verify:package # packs the tarball, installs it offline, uses it as a stranger
npm run bench # regenerates bench/RESULTS.md, takes ~1minprepublishOnly runs the build and the package verification, so the artifact that gets
published is the one just proven installable. Once dist/ exists, npm test also picks
up the compiled-server case, and skips it when it does not.
License
MIT.
This server cannot be deployed
Maintenance
Related MCP Connectors
Deny-by-default authority leases for agents wielding real power.
- llm-busOAuthcom.llm-bus
Coordinate multiple AI agents over MCP: atomic claims, leases, shared ledger, handoffs, tasks.
The backlog that hands out the work: multi-agent task coordination — leases, claims, gates.
- kanonikOAuthai.kanonik
Governance runtime for compliance: verified, human-approved writes to a tamper-evident record.
Related MCP Servers
- AlicenseAqualityAmaintenanceCoherence guard for shared files: when two agents write the same file, the stale writer is denied instead of silently overwriting, then reacquires and retries. Single-host, TLA+-verified.611Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables multiple AI coding agents to safely collaborate in the same git working tree by managing file ownership, merging writes, and preventing snapshot races.10 npm2MIT
- AlicenseNot gradedqualityCmaintenanceProvides a hardened filesystem interface that lets a local model read and write files only within user-specified directories, with symlink protection and atomic operations.18 npm2MIT
- AlicenseCqualityCmaintenanceEnables safe coordination of Herdr coding agents with lease-scoped reviewer and writer lifecycles, semantic waits for agent states, and read-only topology inspection, while prohibiting arbitrary shell execution and unscoped lifecycle operations.44MIT