Skip to main content
Glama

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 status

run 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 fencepost

npm

npm 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 under node_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 build now emits dist/ with declarations, using rewriteRelativeImportExtensions so the source can keep its explicit .ts specifiers.

  • 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 with peerDependenciesMeta.optional is 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's onCompromised never fired, and its release() deleted the lockfile that belonged to the rival.

  • fencepost — the successor got a higher generation, and the wedged holder's gate call returned false and 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. realpath does not unify them while stat reports one (dev, ino) pair, so the true identity is available and unused. Closing it needs multi-key acquisition, which rewrites every signature in lease.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

src/atomic.ts

filesystem primitives, restricted to operations Windows promises

src/record.ts

claim wire format, and the live/expiry rule

src/identity.ts

resource identity — the equality test the whole lock rests on

src/lease.ts

the protocol: acquire, renew, release, and the fence gate

src/cli.ts

command line entry point, no dependencies

src/server.ts

MCP server, mediated writes, path confinement

test/

contention, hard kill, clock steps, corruption, CLI exit codes, MCP round trip

test/verify-package.mjs

installs the built tarball elsewhere and uses it as a stranger would

bench/

latency, contention throughput, the two-process wedged-holder comparison, generated results

docs/

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 ~1min

prepublishOnly 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…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Coherence 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.
      11
      257 PyPI
      14
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables multiple AI coding agents to safely collaborate in the same git working tree by managing file ownership, merging writes, and preventing snapshot races.
      3 npm
      2
      MIT
    • A
      license
      C
      quality
      C
      maintenance
      Enables 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.
      44
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Enables 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.
      15
      38 PyPI
      MIT