hashline

August 22, 2026 Β· View on GitHub

hashline β€” Hash-anchored file editing for AI coding agents

Release CI License Rust

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:

ModeFlagDescription
Compact (default)β€”OK path#hash edits=N changed=N + changed lines only
Verbose--verboseFull file dump after mutation (human-readable)
JSON--jsonStructured 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?

FeatureWhat it does
Agent-first outputCompact, token-minimal by default β€” OK path#hash edits=N + changed lines only
Stable anchorsxxh32 hashes survive nearby edits; re-targeting is one anchor change
Stale-read detectionHard error if file changed between read and patch
Block-aware opsSWAP.BLK / DEL.BLK / INS.BLK.POST for brace-delimited, indent-based, and Ruby def…end blocks
Atomic writesTemp file + rename. No partial writes, no torn edits
Multi-op patchesSeveral SWAP/DEL/INS in one pass via stdin pipe
MCP server6-tool stdio MCP for Claude Code, Codex, Cursor, and friends
Daemon modeBackground JSON-RPC over Unix socket or HTTP
Dry-run preview--dry-run shows diff before applying

How hashline Compares

Dimensionhashlinestr_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/AN/A
SetupSingle Rust binary ~280 Β΅s anchor resolutionBuilt into agentPOSIX 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

PrincipleRationale
Agent-first outputDefault output is compact, token-minimal, machine-readable. --verbose for human debugging.
Anchors over content matchingxxh32 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 errorIf the file changed between read and patch, hashline refuses β€” the agent must re-read and re-anchor. Better fail-fast than corrupt.
Block awarenessBrace-delimited, indentation-based, and Ruby def…end block ops eliminate the "find the closing brace" problem that LLMs struggle with.
Atomic writes onlyTemp file + rename. No partial writes, no torn edits.

Limitation vs Alternatives

Why hashline is not a drop-in for sed or str_replace:

Edge caseReality
Text-search editshashline does not support sed s/old/new/g β€” use sed when you need regex replacement across non-hashable text
Line-number targetinghashline accepts line-number targets as fallback, but the design is anchor-first
Interactive editinghashline 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.

PackageHostToolsInstall
integration/pi-hashlinepi-coding-agentread, edit, write, find_block, remove_file, rename_filepi install npm:hashline-pi
integration/opencode-pluginOpenCodehashline_read, hashline_edit, hashline_write, hashline_find_block, hashline_remove_file, hashline_rename_filenpm 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

PackageInstall
integration/opencode-pluginopencode.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

CommandDescriptionSee also
readRead file with [path#HASH] + `LINE:hashcontent`
patchApply SWAP/DEL/INS/BLK editshashline guide β†’ Patch Operations
writeWrite content to a new file (--force overwrites)
find-blockFind enclosing brace/indent/Ruby block around anchor
removeDelete a file
renameRename (move) a file
removeDelete a file
guideInteractive user guide β€” always matches your binarybuilt-in
servedaemon over Unix socket or HTTPhashline guide β†’ Daemon Mode
mcpMCP 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:

InputOutputWhen to use
++text+textContent that literally starts with a + sign
+-text-textContent 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 caseReality
Not a sed replacementhashline does not support regex find-and-replace across text β€” use sed for that
Anchor-first designLine-number targeting works as fallback, but the tool is optimized for hash-based edits
Batch-orientedread β†’ patch workflow, not interactive editing
No tree-sitterBlock resolution is syntactic (brace depth, indent, end), not AST-based
ErrorLikely CauseFix
I/O error: No such file or directoryPath does not existCheck path + permissions
line 2 content changed since last readFile modified after readhashline read <file> retry patch
hash 'ff' not found in demo.txtAnchor copied from wrong readRe-read + copy fresh hash
hash 'ab' matches 3 lines4-hex hash is ambiguousUse 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.