Tamper-evident agent handoff

Verifiable handoffs between agents.

Padlock is an append-only, hash-chained locker for passing work between agents. One agent writes a structured handoff, seals the pad, and hands off a read ticket. Another agent reads it back and verifies the chain end-to-end before the content ever reaches a model's context.

What Padlock does

Padlock gives two agents a shared, ordered record of a handoff — and a way for the receiver to check that nothing in that record was altered after it was written.

  1. 01 · WRITE

    Write the handoff

    The sending agent appends blocks to a pad. Block 0 is a strict locker.handoff.v1 envelope: task id, from/to agents, constraints, artifacts, budget.

  2. 02 · SEAL

    Seal the pad

    Sealing freezes the pad and revokes further writes. The pad's head_hash — the hash of its latest block — becomes fixed.

  3. 03 · HANDOFF

    Pass a read ticket

    The receiver gets a scoped read ticket, not the write key. A read_once ticket opens a short read lease; a read_unlimited ticket can be minted for a durable audit.

  4. 04 · VERIFY

    Verify the chain

    The client walks the chain from the genesis hash to head_hash. If every link matches, the record is internally consistent — the receiver knows the blocks it is about to use are the blocks that were written.

An example handoff

Each block carries prev_hash and curr_hash, so verification is self-contained: a reader needs only the block list.

Illustrative Example chain for a reviewer handoff. Hash values are shortened examples, not a live pad.
Block 0 Handoff envelope — locker.handoff.v1 prev_hash 00000000…00000000 (genesis)  ·  curr_hash 9c1f4a…e7b2
Block 1 Implementation attestation — free-form JSON prev_hash 9c1f4a…e7b2  ·  curr_hash 41ab0d…55c8
Block 2 Reviewer notes prev_hash 41ab0d…55c8  ·  curr_hash 7de310…1a94  ← head_hash
Block 0 — handoff envelope (illustrative)
{
  "schema": "locker.handoff.v1",
  "task_id": "refactor-auth-42",
  "from_agent": "planner",
  "to_agent": "worker",
  "constraints": ["no schema changes", "tests must stay green"],
  "artifacts": [{ "name": "plan.md", "size": 2048 }],
  "budget_usd": 1.25
}
Block 1 — attestation, recommended shape (illustrative)
{
  "task_id": "refactor-auth-42",
  "from_agent": "worker",
  "to_agent": "reviewer",
  "status": "completed",
  "canonical_ref": { "repo": "owner/name", "branch": "main", "commit_sha": "…" },
  "verification": { "test_command": "pytest -q", "exit_code": 0, "tests_passed": 12, "tests_failed": 0 },
  "environment": { "runtime": "python 3.11.8", "dependency_manifest": "requirements.txt", "dependency_hash": "sha256:…" }
}

Block 0 is schema-validated and rejected unless every field is present and correctly typed. Block 1 and later blocks are free-form — the daemon does not validate them. The attestation above is a recommended shape, not an enforced one.

Install and configure

Padlock has two parts. lockermcp is the MCP client your agent talks to; lockerd is the daemon that stores pads. The client is published on PyPI; the daemon you run yourself. Each is a normal Python package.

Three ways to use Padlock — and what each one actually is

1 · Self-host the published package Recommended

The full API on infrastructure you control, including POST /v1/pads/<id>/tickets. This is the path for real work: your pads never leave your machine unless you expose the daemon.

2 · Inspect the public read-only demo

A seeded, sealed pad served from the hosted instance. No account, no writes, no ticket — for looking at a real hash chain and the response shape before installing anything.

3 · The hosted API — read-only

A sample instance, not a service. Three paths are public — health, the demo pad's blocks and its manifest — and only GET/HEAD. Every other path and method returns 403 at its edge, writes included; the API schema and other pads are not public. It reports version 0.1.0, has no /tickets endpoint, and carries no support commitment. Use it to look, not to work.

1 · The MCP client (published package)

uvx lockermcp runs the client straight from PyPI without installing it. This is the published client command — it is not a standalone hosted service.

Run the MCP client (stdio)
uvx lockermcp

2 · Register it with an MCP client

The client's only required configuration is LOCKER_URL — the base URL of the daemon it should talk to.

mcpServers block
{
  "mcpServers": {
    "lockermcp": {
      "command": "uvx",
      "args": ["lockermcp"],
      "env": { "LOCKER_URL": "http://127.0.0.1:8000" }
    }
  }
}

3 · Run your own daemon

The daemon binds 127.0.0.1:8000 by default and stores pads in a single SQLite file. Use the same variable, LOCKER_URL, to point the client at it.

Run the daemon (from the published package)
uvx --from lockermcp lockerd
# or, in a virtualenv:
pip install lockermcp
LOCKER_DB_PATH=locker.db LOCKER_MODE=open lockerd

Six tools are exposed over stdio: locker_deposit (create + append + seal in one call), locker_create, locker_append, locker_seal, locker_manifest, and locker_read_blocks. Reads verify the chain in Python and return an integrity result — the model never computes a hash itself.

Hosted instance vs. published package. The hosted API at api.padlockspace.org is read-only: exactly three paths are public (/health, the demo pad's blocks and manifest), only GET/HEAD, and everything else is 403 — so a client pointed at it cannot deposit a handoff. It reports version 0.1.0 and has no POST /v1/pads/<id>/tickets endpoint — both of which the published package provides. The discovery configuration on this site therefore defaults LOCKER_URL to your own daemon, http://127.0.0.1:8000.
0.1.4 — upgrade if your daemon accepts writes. In 0.1.3 an append could return 201 and still not persist: GET /health ran its own transaction on the same database connection the append was using, and that rollback discarded the in-flight block while the append went on to commit the pad's head and byte count. Health no longer touches transaction state, every route rolls back on any failure or cancellation, creating a pad and its first read ticket is one transaction, and there is a 4,096 byte limit on JSON request bodies for pad creation and ticket minting, enforced before parsing. Upgrading stops the loss; it does not restore writes already lost. The hosted demo is unaffected — it is read-only and accepts no writes.

Practical uses

Padlock is a primitive, not a platform. It fits wherever one automated step hands work to another and the receiver benefits from a checkable record.

Multi-agent coding pipelines

A planner hands a scoped task to a worker; a reviewer receives the change and its test result as one sealed record.

Review and approval gates

Use allowed_paths to declare the permitted file scope, so a reviewer can flag any change that lands outside it.

Tamper-evident audit trails

Seal a pad and keep its head_hash yourself. Any later rewrite of the pad no longer matches the head you retained.

Reproducible attestations

Peg a run to an exact dependency snapshot with environment.dependency_hash so an independent re-run reproduces the result instead of whatever upstream installs today.

Padlock does not claim integrations it doesn't have. It speaks MCP over stdio, and the repository README documents how to register it with an MCP client.

Integrity guarantees — and their limits

The chain is a precise tool with a precise scope. It is worth stating both halves plainly.

Establishes What the chain proves

  • Internal consistency. Each block's hash is bound to its predecessor, so a single mutated payload breaks verification: locker_read_blocks and locker_manifest return a verification_failed error and no payloads at all. The check runs in the client before blocks enter model context.
  • Change detection against a trusted head. If a reader retains a pad's head_hash independently, it can detect any later rewrite of that pad.

Does not What the chain can't prove

  • It does not authenticate the writer. from_agent / to_agent are client-supplied strings. Holding the write key proves possession of the key, not identity.
  • It does not make an attestation true. Block 1 is free-form and unvalidated: exit_code, tests_passed and commit_sha are claims the writer makes about itself.
  • It does not defend against a malicious operator. Whoever runs the daemon can rewrite both the chain and the head it serves. It is not a substitute for decentralized consensus against a hostile host.
In one line: verification detects internal inconsistency. Detecting a rewritten chain requires a trusted head you retained independently.

Inspect a live pad

The hosted daemon seeds a permanent read-only pad, so you can look at a real hash chain without creating anything or paying. It is read-only: writing to it is refused.

Read the demo pad (no ticket required)
curl https://api.padlockspace.org/v1/pads/demo-pad-v1/blocks

Or open the demo endpoint directly. This page does not fetch it in your browser — you can read the raw JSON yourself.

Status. Verified reachable and read-only as of this build: the endpoint returns HTTP 200 with three blocks, and the instance accepts only GET/HEAD — writes are refused. It is an older daemon build than the published package (version 0.1.0, no /tickets endpoint), so treat it as a sample of the data shape and of a live chain — not as a feature-complete service.

Discuss a paid pilot

Padlock ships as a self-hosted primitive. If you want it wired into a handoff you already run, I take on a small number of fixed-scope integration pilots.

What a pilot covers

  • Integrate Padlock into one existing agent-to-agent handoff.
  • Configure the agreed deployment and verification workflow.
  • Provide operating instructions and a defined support window.
  • Agree scope, acceptance criteria, schedule, and a fixed price before work begins.

What “done” looks like

An acceptance test adapted to your setup, for example: one agent seals a handoff, another retrieves and verifies it, altered payloads fail verification, and your team can repeat the workflow independently.

Scope, acceptance criteria, schedule, and a fixed price are agreed in writing before any work starts. No outcome is promised beyond the criteria we set together.

Start a conversation

Email mccullough.sheldon@gmail.com with the subject “Padlock pilot inquiry”.

Address (selectable): mccullough.sheldon@gmail.com

Tell me which agent tools you use, how you hand work off today, and what breaks. That is usually enough for me to say whether a pilot is a fit.

A paid pilot is not a licence. A pilot buys integration work; it does not include or grant a commercial licence, and paying for one does not change the AGPL-3.0-or-later terms the software ships under. Commercial licensing is quoted separately on request, to the same address: mccullough.sheldon@gmail.com.