hashline
August 22, 2026 Β· View on GitHub
Hash-anchored file editing for Claude Code, AI coding agents, and patch-safe automation.
Every line gets a stable xxh32 hash (42:a3). Patch by anchor, not by fragile text match. Stale reads are caught and rejected before they corrupt your work.
curl -fsSL "https://raw.githubusercontent.com/quangdang46/hashline/main/install.sh?$(date +%s)" | bash
π€ Agent Quickstart (MCP / Robot Mode)
hashline ships a 6-tool MCP server that works with Claude Code, Codex, Cursor, Windsurf, Gemini CLI, and OpenCode. The installer auto-configures it.
# MCP stdio server (auto-wired by installer)
hashline mcp
# Read a file with hashes β agents copy anchors, not lines
hashline read src/auth.js
# Patch by anchor β survives nearby edits (compact output)
hashline patch src/auth.js 'SWAP 2:b2:
+ const decoded = jwt.verify(token, env.SECRET)'
# OK src/auth.js#7f2a edits=1 changed=1
# ~2:f9| const decoded = jwt.verify(token, env.SECRET)
# Dry-run before applying
hashline patch src/auth.js 'DEL 3' --dry-run
Output architecture β agent-first, token-minimal by default:
| Mode | Flag | Description |
|---|---|---|
| Compact (default) | β | OK path#hash edits=N changed=N + changed lines only |
| Verbose | --verbose | Full file dump after mutation (human-readable) |
| JSON | --json | Structured JSON with changed lines array |
Output conventions
- stdout = data only (file content, patch result, JSON)
- stderr = diagnostics, warnings (
ERR KIND key=val+HINT ...in compact mode) - exit 0 = success, exit 1 = stale-read rejection or no-op
TL;DR
The Problem
AI coding agents (str_replace, sed, bespoke edit LLM tools) routinely botch file edits. The pattern is always the same: whitespace mismatch, stale context, a } that was supposed to close a block but grabbed the wrong one instead. Each failure costs 10β60 seconds in retry round-trips, and after the first successful edit, every remaining line number shifts β so targeting by number alone is fragile.
The Solution
hashline replaces fragile text-matching with content-hashed line anchors (42:a3). Read a file once and every line comes with a stable xxh32 hash. Patch using those anchors β insertions, deletions, swaps, and block replacements all reference hashes, not line text or numbers. If the file changed between read and apply, hashline rejects the patch with a clear error. No silent corruption, no wasted retries.
Why hashline?
| Feature | What it does |
|---|---|
| Agent-first output | Compact, token-minimal by default β OK path#hash edits=N + changed lines only |
| Stable anchors | xxh32 hashes survive nearby edits; re-targeting is one anchor change |
| Stale-read detection | Hard error if file changed between read and patch |
| Block-aware ops | SWAP.BLK / DEL.BLK / INS.BLK.POST for brace-delimited, indent-based, and Ruby defβ¦end blocks |
| Atomic writes | Temp file + rename. No partial writes, no torn edits |
| Multi-op patches | Several SWAP/DEL/INS in one pass via stdin pipe |
| MCP server | 6-tool stdio MCP for Claude Code, Codex, Cursor, and friends |
| Daemon mode | Background JSON-RPC over Unix socket or HTTP |
| Dry-run preview | --dry-run shows diff before applying |
How hashline Compares
| Dimension | hashline | str_replace (built-in) | sed |
|---|---|---|---|
| Stable anchors | β
xxh32 hash 42:a3 | β Exact text match | β Fragile regex |
| Stale-read detection | β Hard error on mismatch | β Applies blindly | β Applies blindly |
| Block replacement | β SWAP.BLK / DEL.BLK / INS.BLK.POST | β Line-granularity | β Line-granularity |
| Atomic writes | β Temp file + rename | β Temp file + rename | β In-place (torn writes possible) |
| Multi-op batches | β
stdin *** Begin Patch | β One replacement per call | β
-e flag chaining |
| Dry-run preview | β
--dry-run with diff | β Not supported | β Not supported |
| MCP server | β
hashline mcp (6 tools) | N/A | N/A |
| Setup | Single Rust binary ~280 Β΅s anchor resolution | Built into agent | POSIX standard |
Quick Example
# 1. Read a file β every line gets a hash
hashline read src/app.ts
# src/app.ts#1A2B
# 1:a1|import { verify } from 'jwt'
# 2:b2|const token = req.headers.authorization
# 3:c3|if (!verify(token, SECRET)) throw 401
# 4:d4|return decode(token)
# 2. Build a patch using the anchor (compact output)
hashline patch src/app.ts 'SWAP 3:c3:
+ if (!token) throw new AuthError("missing token")'
# OK src/app.ts#7f2a edits=1 changed=1
# ~3:e5| if (!token) throw new AuthError("missing token")
# 3. Human-readable mode (full file after patch)
hashline patch src/app.ts --verbose 'SWAP 4:d4:
+ return decode(token)'
# 4. Structured JSON output
hashline patch src/app.ts --json 'SWAP 3:c3:
+ if (!token) throw new AuthError("missing token")'
Design Philosophy
| Principle | Rationale |
|---|---|
| Agent-first output | Default output is compact, token-minimal, machine-readable. --verbose for human debugging. |
| Anchors over content matching | xxh32 hashes are stable, short, and easy for agents to copy. Re-targeting after an edit is a single anchor change. |
| Stale-read is a hard error | If the file changed between read and patch, hashline refuses β the agent must re-read and re-anchor. Better fail-fast than corrupt. |
| Block awareness | Brace-delimited, indentation-based, and Ruby defβ¦end block ops eliminate the "find the closing brace" problem that LLMs struggle with. |
| Atomic writes only | Temp file + rename. No partial writes, no torn edits. |
Limitation vs Alternatives
Why hashline is not a drop-in for sed or str_replace:
| Edge case | Reality |
|---|---|
| Text-search edits | hashline does not support sed s/old/new/g β use sed when you need regex replacement across non-hashable text |
| Line-number targeting | hashline accepts line-number targets as fallback, but the design is anchor-first |
| Interactive editing | hashline is batch-oriented (read β patch) β for interactive editing use your editor |
Credit
hashline is developed based on the idea of hash-anchored line editing. Thanks to can1357 for the original oh-my-pi.
Installation
# macOS / Linux β curl pipe
curl -fsSL "https://raw.githubusercontent.com/quangdang46/hashline/main/install.sh?$(date +%s)" | bash
# Windows PowerShell
irm "https://raw.githubusercontent.com/quangdang46/hashline/main/install.ps1" | iex
# From source
cargo install --path crates/core
The installers auto-detect your platform, fetch the matching binary from GitHub Releases, verify the SHA-256, and atomically install to ~/.local/bin/hashline. They also auto-detect supported MCP hosts (claude-code, codex, cursor, windsurf, vscode, gemini, opencode) and upsert a hashline MCP server entry for each.
Agent Host Integrations
Beyond the MCP server, hashline ships thin-wrapper packages for agent hosts that
prefer native read/edit tools over MCP. Both shell out to the hashline binary β
they never reimplement hashing, staleness detection, or merge recovery in TypeScript.
| Package | Host | Tools | Install |
|---|---|---|---|
integration/pi-hashline | pi-coding-agent | read, edit, write, find_block, remove_file, rename_file | pi install npm:hashline-pi |
integration/opencode-plugin | OpenCode | hashline_read, hashline_edit, hashline_write, hashline_find_block, hashline_remove_file, hashline_rename_file | npm i hashline-opencode-plugin + opencode.json "plugin": ["hashline-opencode-plugin"], disable native edit |
pi-coding-agent guide
The hashline-pi extension replaces pi's built-in
file tools with the full hashline surface β anchors on every read, stale-safe batched edits,
tree-sitter block ops, and colored diffs in the TUI.
# 1. Install the binary first (the package is a thin wrapper β it does not bundle it)
curl -fsSL "https://raw.githubusercontent.com/quangdang46/hashline/main/install.sh" | bash # macOS / Linux
irm "https://raw.githubusercontent.com/quangdang46/hashline/main/install.ps1" | iex # Windows (PowerShell)
# 2. Install the extension (project-local: add -l; global: omit it)
pi install npm:hashline-pi
Then /reload in pi and check /hashline-status. Requires binary >= 0.9.12.
Binary off PATH? Set HASHLINE_BIN or { "binary": "..." } in ~/.pi/agent/hashline.json.
Full details: integration/pi-hashline/README.md.
OpenCode
| Package | Install |
|---|---|
integration/opencode-plugin | opencode.json plugin: ["@scope/hashline-opencode-plugin"] + disable native edit |
See integration/opencode-plugin/README.md β published as hashline-opencode-plugin on npm. |
Both packages require the hashline binary on PATH (or HASHLINE_BIN). See each package's
README.md and integration/CONTRACT.md for the exact CLI contract.
Quick Start
# 1. Read a file with snapshot hashes
hashline read src/auth.js
# 2. Apply a single-line patch
hashline patch src/auth.js 'SWAP 2:
+ const decoded = jwt.verify(token, env.SECRET)'
# 3. Apply a range
hashline patch src/auth.js 'SWAP 2..4:
+ return decoded'
# 4. Delete a line
hashline patch src/auth.js 'DEL 3'
# 5. Dry-run first
hashline patch src/auth.js 'DEL 3' --dry-run
# 6. Block operations
hashline patch src/mod.rs 'SWAP.BLK 12:
+fn replaced() {
+ // new body
+}'
# 7. Multi-op via stdin (no intermediate file)
hashline patch src/auth.js - <<'EOF'
*** Begin Patch
SWAP 5:1a2b:
+ const decoded = jwt.verify(token, env.SECRET)
DEL 9
*** End Patch
EOF
Commands
| Command | Description | See also |
|---|---|---|
read | Read file with [path#HASH] + `LINE:hash | content` |
patch | Apply SWAP/DEL/INS/BLK edits | hashline guide β Patch Operations |
write | Write content to a new file (--force overwrites) | |
find-block | Find enclosing brace/indent/Ruby block around anchor | |
remove | Delete a file | |
rename | Rename (move) a file | |
remove | Delete a file | |
guide | Interactive user guide β always matches your binary | built-in |
serve | daemon over Unix socket or HTTP | hashline guide β Daemon Mode |
mcp | MCP stdio server (6 tools) | hashline guide β MCP Mode |
Architecture
βββββββββββββββββββ
β hashline read β
β [file#1A2B] β
β 1:a1|content β
ββββββββββ¬βββββββββ
β copy anchor
βΌ
ββββββββββββββββββββββββββββ
β Build patch string β
β SWAP 2:b2: β
β +new content β
ββββββββββ¬ββββββββββββββββββ
β
ββββββββββΌββββββββββ ββββββββββββββββ
β hashline patch ββββ --dry-run β
β file.patch β β preview β
ββββββββββ¬ββββββββββ ββββββββββββββββ
β
ββββββββββΌββββββββββ
β File updated β
β (atomic write) β
ββββββββββββββββββββ
Block-aware resolution by extension:
.rs .js .ts .go .java .c .cpp .h .cs β brace-balanced { }
.py .verse β indentation-based
.rb β def β¦ end matching
Payload Escapes
Payload lines starting with + have the + prefix consumed as a sigil marker. To produce a literal leading + or -, use the escapes:
| Input | Output | When to use |
|---|---|---|
++text | +text | Content that literally starts with a + sign |
+-text | -text | Content that literally starts with a - (e.g. Markdown list items). Without the escape, bare - lines emit a warning but are still preserved. |
A blank line inside a payload block is written as a bare empty line (no + prefix):
INS.POST 2:
+First paragraph.
+Second paragraph.
Limitations
| Edge case | Reality |
|---|---|
| Not a sed replacement | hashline does not support regex find-and-replace across text β use sed for that |
| Anchor-first design | Line-number targeting works as fallback, but the tool is optimized for hash-based edits |
| Batch-oriented | read β patch workflow, not interactive editing |
| No tree-sitter | Block resolution is syntactic (brace depth, indent, end), not AST-based |
| Error | Likely Cause | Fix |
|---|---|---|
I/O error: No such file or directory | Path does not exist | Check path + permissions |
line 2 content changed since last read | File modified after read | hashline read <file> retry patch |
hash 'ff' not found in demo.txt | Anchor copied from wrong read | Re-read + copy fresh hash |
hash 'ab' matches 3 lines | 4-hex hash is ambiguous | Use line-qualified 2:ab |
FAQ
Does hashline work with Claude Code's built-in tools? Yes β hashline mcp exposes a stdio MCP server with 6 tools (read, patch, write, find_block, remove_file, rename_file) that any MCP-capable agent can call. The install script auto-configures it.
Can I use hashline as a daemon? Yes β hashline serve runs a background daemon that accepts JSON-RPC over Unix socket (default: ~/.hashline/daemon.sock) or HTTP (--http 17300). Set HASHLINE_URL to route CLI calls through it.
Is it fast? Anchor resolution on a 10k-line file takes ~280 Β΅s. Full patch (parse + apply) is ~297 Β΅s. File I/O dominates at scale, not hashing.
What about tree-sitter? hashline does not use tree-sitter. Block resolution is purely syntactic (brace depth, indentation, end keyword). This keeps the binary small and startup instant.