fencepost
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
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.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Renewable exclusive leases and semaphores for workers; downstream fencing enforcement required.
Deny-by-default authority leases for agents wielding real power.
Coordinate multiple AI agents over MCP: atomic claims, leases, shared ledger, handoffs, tasks.
Governance runtime for compliance: verified, human-approved writes to a tamper-evident record.
Related MCP Servers
- AlicenseAqualityBmaintenanceCoherence 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.11257 PyPI14Apache 2.0
- AlicenseNot gradedqualityDmaintenanceEnables multiple AI coding agents to safely collaborate in the same git working tree by managing file ownership, merging writes, and preventing snapshot races.3 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
- AlicenseBqualityBmaintenanceEnables multiple coding agents sharing one repository to coordinate their work through time-limited directory and file leases, an enforced pre-tool-use hook, shared presence tracking, messaging, and an audit history, so they never edit or run each other's files concurrently.1538 PyPIMIT