resilient-write
An MCP server that gives coding agents a durable, fault-tolerant write surface so they can keep making forward progress when a tool call is blocked by a content filter, a size cap, or an opaque transport error.
Why this exists
Coding agents today write through a single tool like Write or edit_file.
When that tool rejects a payload (for any reason), the failure mode is usually:
- The agent loses the content it was trying to write (it only exists in model memory).
- There is no structured error, so the agent can't reason about what to change.
- Retries thrash because the agent re-sends the exact same rejected content.
- Any downstream step that depended on the file is now operating on half-broken state.
- No handoff mechanism exists, so a fresh agent or sibling agent has to re-derive the context.
This project addresses those five failure modes as five orthogonal layers.
Layered architecture (sixteen MCP tools, one convention file)
| Layer | Tool name | What it does | Catches |
|---|---|---|---|
| L0 | rw.risk_score |
Static pre-flight classifier over draft content (regexes + length + binary heuristics). Returns a numeric risk score plus a list of detected patterns. | Secret-shaped strings, long lines, encoding issues before they hit the sink. |
| L1 | rw.safe_write |
Transactional write: temp file + hash verify + atomic rename + journal append. | Half-written files, lost prior content, no audit trail. |
| L2 | rw.chunk_write |
Write one numbered chunk to a session directory via safe_write; idempotent retries. | Single-chunk failures without losing prior chunks. |
| L2 | rw.chunk_append |
Auto-incrementing chunk write — detects the highest index and writes index+1. | Misnumbered chunks; lets the agent stream sections without tracking indices. |
| L2 | rw.chunk_compose |
Concatenate a session's chunks in order and write the result through safe_write. | Any single-call write that is too large or too risky; allows incremental progress with rollback. |
| L2 | rw.chunk_reset |
Destructively wipe an in-progress chunk session. | Stale session state after an abandoned compose. |
| L2 | rw.chunk_status |
Report which chunk indices are present and what total was declared. | Missing or duplicate chunks before compose. |
| L2 | rw.chunk_preview |
Dry-run compose — returns concatenated content without writing to disk. | Pre-write validation; pair with rw.validate to catch errors before commit. |
| L3 | rw.typed_error (schema) |
Specification for structured tool errors {reason_hint, detected_patterns, suggested_action, retry_budget}. Wraps safe_write and chunk_compose. |
Opaque tool-harness errors that agents cannot reason about. |
| L4 | rw.scratch_put |
Store raw material out-of-band, content-addressed by SHA-256. | Cases where the content legitimately does not belong in the workspace. |
| L4 | rw.scratch_ref |
Look up a scratchpad entry by hash or label without returning content. | Verify what is stored before deciding to surface it. |
| L4 | rw.scratch_get |
Retrieve raw content by hash (disableable via $RW_SCRATCH_DISABLE_GET). |
Controlled retrieval; supports write-only mode. |
| L5 | rw.handoff_write |
Write a HANDOFF.md continuity envelope (YAML front-matter + body). Reports drift warnings. |
Cross-agent and cross-session continuity when a task is interrupted. |
| L5 | rw.handoff_read |
Parse a HANDOFF.md envelope and return structured front-matter plus body. |
Picking up where another agent left off. |
| — | rw.journal_tail |
Inspection helper — last N rows of the L1 write journal, with optional filters. | Debugging write history and audit. |
| — | rw.validate |
Format-aware syntax validator (LaTeX, JSON, Python, YAML). | Structural errors (unbalanced braces, bad parses) before they reach disk. |
| — | rw.analytics |
Journal analytics — write counts, timing, hot paths, session summaries. | Understanding agent write patterns and diagnosing performance issues. |
Layers can be adopted independently. The minimum useful install is L1 + L5.
Install
pip install resilient-write
Or run directly:
uvx resilient-write
Transports
The server supports three transports selected with --transport:
| Flag | Protocol | When to use |
|---|---|---|
--transport stdio |
stdin/stdout (default) | Local MCP clients (Claude Code, Cursor, Codex) |
--transport sse |
HTTP + Server-Sent Events | Remote clients, browser-based tooling |
--transport http |
Streamable HTTP | Remote clients that need session resumability |
stdio (default)
resilient-write
# or explicitly:
resilient-write --transport stdio
SSE
resilient-write --transport sse --host 127.0.0.1 --port 8000
Exposes two endpoints:
GET /sse— client connects here to open the event streamPOST /messages/— client posts JSON-RPC messages here
Streamable HTTP
resilient-write --transport http --host 127.0.0.1 --port 8000
Exposes a single endpoint:
POST/GET /mcp— all JSON-RPC traffic, with SSE streaming in responses
Sessions are tracked server-side. Reconnecting clients resume their session via the Mcp-Session-Id response header.
Path resolution: CWD, workspaces, state
Three independent concepts:
| Env var / source | Purpose | Default |
|---|---|---|
| CWD (process working directory) | The default anchor for relative paths supplied by the agent | inherited from the launcher |
RW_WORKSPACE |
The access boundary — paths must resolve inside one of these roots | CWD |
RW_STATE_DIR |
Where .resilient_write/ state lives (journal, chunks, checkpoints, scratchpad) |
CWD |
Resolution rules
- Relative paths anchor at CWD (not at
workspaces[0]).safe_write(path="src/foo.py")writes to<CWD>/src/foo.py. - Absolute paths are accepted only if they fall inside some workspace root. This is the escape hatch for cross-workspace writes.
- The workspace boundary is checked against the full set of roots — target must be inside any one of them.
- CWD is auto-added to the workspace list if it isn't already inside one of the configured roots, so a plain relative path is always within the access boundary.
- State is decoupled from workspaces. Set
RW_STATE_DIRto put state somewhere specific; otherwise it lives in CWD.
RW_WORKSPACE accepts:
- Plain string:
"/Users/jay/my-project" os.pathseplist:"/Users/jay/a:/Volumes/Lux/dev/b"- JSON array:
["/Users/jay/a", "/Volumes/Lux/dev/b"]
CLI
# Single workspace
resilient-write -w /Users/jay/proj-a
# Multiple workspaces (repeatable)
resilient-write -w /Users/jay/proj-a -w /Volumes/Lux/dev/proj-b
CLI args prepend to $RW_WORKSPACE.
MCP client configuration
stdio (Claude Code, Cursor, Codex)
{
"mcpServers": {
"resilient-write": {
"command": "uvx",
"args": ["resilient-write"],
"env": {
"RW_WORKSPACE": "[\"/Users/jay/proj-a\", \"/Volumes/Lux/dev/proj-b\"]"
}
}
}
}
Running from a checkout: use --project, not --directory
When launching from a local source checkout via uv run, use --project, not --directory:
{
"command": "uv",
"args": ["run", "--project", "/path/to/resilient-write-src", "resilient-write"],
"env": { "RW_WORKSPACE": "[\"/Users/jay\"]" }
}
uv run --directory X chdir's to X before running, which pins the server's
CWD to its own source tree — relative paths from the agent then resolve there
rather than in the agent's project. --project X locates the package without
changing CWD, so the server inherits the launcher's CWD (your active project).
For any MCP server that resolves user-supplied relative paths, prefer
--project.
RW_STATE_DIR is optional — when unset, state lives in CWD. Set it to a
fixed location if you want a single journal across all projects, or leave it
unset for per-project state.
SSE
{
"mcpServers": {
"resilient-write": {
"type": "sse",
"url": "http://127.0.0.1:8000/sse"
}
}
}
Streamable HTTP
{
"mcpServers": {
"resilient-write": {
"type": "http",
"url": "http://127.0.0.1:8000/mcp"
}
}
}
See docs/INSTALL.md for full setup instructions for all clients.
Usage
Basic: write a file safely
The agent calls rw.safe_write instead of raw Write. The file is written atomically (temp → fsync → verify → rename) and logged to the audit journal.
rw.safe_write(path="src/main.py", content="print('hello')", mode="create")
→ {"ok": true, "path": "src/main.py", "sha256": "a1b2c3...", "bytes": 14}
Pre-flight risk check
Before writing content that might contain tokens or credentials, run rw.risk_score:
rw.risk_score(content="Bearer sk-ant-oat01-AAAA...")
→ {"ok": true, "score": 0.82, "verdict": "high",
"detected_patterns": [{"kind": "api_key", ...}],
"suggested_actions": [{"action": "redact", "targets": ["api_key"]}]}
If the verdict is high, redact the flagged patterns before writing.
Large files: chunked writes
For files over ~5KB, build them section by section:
rw.chunk_append(session="my-report", content="# Introduction\n...")
rw.chunk_append(session="my-report", content="# Methods\n...")
rw.chunk_append(session="my-report", content="# Results\n...")
# Preview before committing
rw.chunk_preview(session="my-report")
# Validate syntax
rw.validate(content=<preview_content>, format_hint="latex")
# Compose the final file
rw.chunk_compose(session="my-report", output_path="report.tex", cleanup=true)
If a chunk fails mid-session, prior chunks are already on disk — just retry the failing one.
Handling errors
Every failure returns a structured envelope the agent can branch on:
{
"ok": false,
"error": "blocked",
"reason_hint": "content_filter",
"detected_patterns": ["api_key"],
"suggested_action": "redact",
"retry_budget": 2
}
The agent reads suggested_action and acts accordingly — no guesswork, no blind retries.
Sensitive content: scratchpad
Store raw credentials or PII out-of-band instead of writing them to the workspace:
rw.scratch_put(content="sk-ant-oat01-real-key-here", label="captured-token")
→ {"ok": true, "sha256": "5c9a3b...", "dedup": false}
The .resilient_write/ directory is gitignored. Reference the hash in your report instead of the raw value.
Session handoff
Before ending a session (or when blocked), save state for the next agent:
rw.handoff_write(envelope={
"task_id": "my-report",
"status": "partial",
"agent": "claude-opus-4-6",
"summary": "Sections 1-3 complete, section 4 blocked on content filter",
"next_steps": ["Redact api_key patterns in section 4", "Retry chunk 4"],
"last_good_state": [{"path": "report.tex", "sha256": "4b0c12..."}]
})
A fresh agent calls rw.handoff_read to pick up where you left off.
Making agents prefer rw.* tools automatically
Drop a CLAUDE.md (for Claude Code) or .cursorrules (for Cursor) in your project root. The server also sends MCP-level instructions at initialization, telling compatible clients to prefer rw.* tools over raw writes. See the included CLAUDE.md for the recommended content.
Status
- Architecture document
- Per-layer specs
- JSON schemas (
spec/errors.schema.json) - Reference Python implementation (all six layers, 16 tools)
- Test suite (211 tests, all green)
- Published MCP config snippets (
docs/INSTALL.md) - Published to PyPI
- HTTP and SSE transport support (
--transport sse|http)
Citation
If you use resilient-write in your research, please cite:
@article{agyemang2026resilientwrite,
title = {Resilient Write: A Six-Layer Durable Write Surface for LLM Coding Agents},
author = {Owusu Agyemang, Justice and Kponyo, Jerry John and Amponsah, Elliot and Addo Boakye, Godfred Manu and Obour Agyekum, Kwame Opuni-Boachie},
journal = {arXiv preprint arXiv:2604.10842},
year = {2026},
url = {https://arxiv.org/abs/2604.10842}
}
Origin
This project was spun out of a concrete failure observed while producing an
LLM-CLI telemetry analysis report. A Write tool call was silently rejected
when the draft contained redacted-looking credential strings; the agent
recovered only after five retries and a hand-written chunked-append workaround.
The five layers here correspond to the five things that would have caught
that failure before it wasted cycles. See docs/SCENARIOS.md for the full
postmortem.
Metadata
Release files for resilient-write 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| resilient_write-0.3.0.tar.gz | 104.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| resilient_write-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 152.9 kB
Release files / resilient_write-0.3.0.tar.gz
| Download URL | resilient_write-0.3.0.tar.gz |
|---|---|
| Size | 104.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
63aa0675f7fe30c3a0862c2068f0581e97cff842bacdd87b5e509dec77f396a0
|
|
BLAKE2b-256 checksum How to use checksums |
8b99fd839067a07e6328ea5acb6d5113790e134b68dc137548a0a677fe8a27b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.10.18
|
Release files / resilient_write-0.3.0-py3-none-any.whl
| Download URL | resilient_write-0.3.0-py3-none-any.whl |
|---|---|
| Size | 48.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
74080a22fc4084c4809e222441a2058e884315e1edb0bdba43b0c8e7ed33de55
|
|
BLAKE2b-256 checksum How to use checksums |
9a880d69814ebf5f85577e25640fe55ad68f649024eea9a17e8330f883e10f5e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.10.18
|