Stable line-addressed file reading and editing for Claude Code, AI coding agents, and patch-safe automation. Every line gets a 2-char content hash, so edits target anchors instead of fragile whitespace-exact string replacement.
linehash is a Rust CLI for safe file editing with content-hashed line anchors. It helps Claude Code and other AI coding tools read files, locate lines, apply edits, and reject stale changes before they corrupt code.
- Built for Claude Code and AI coding agents
- Safer than
str_replacefor file editing and patch workflows - Uses content-hashed line anchors instead of fragile exact-text matching
- Detects stale reads, ambiguous anchors, and concurrent file changes
- Written in Rust with simple CLI and JSON output for automation
| Tool / workflow | How it locates code | Main failure mode | Best use case |
|---|---|---|---|
str_replace |
Exact old text match | Fails when whitespace or formatting differs | Small literal replacements when exact text is known |
| Unified diff / patch | Context lines around a hunk | Hunks can fail or apply badly after nearby edits | Reviewable multi-line changes and code review workflows |
linehash |
Content-hashed line anchors like 12:ab |
Rejects stale or ambiguous anchors instead of guessing | Safe AI-assisted file editing, targeted edits, and patch-safe automation |
Why this matters for AI coding: models often know what to change but are less reliable at reproducing the exact old text required by str_replace. linehash reduces that failure mode by letting tools edit by anchor, verify file state, and stop on stale reads before code is corrupted.
Claude Code uses str_replace to edit files — the model must reproduce the exact old text,
character by character, including whitespace and indentation.
The "String to replace not found in file" error has its own GitHub issues megathread with 27+ related issues. It's not the model being dumb — it's the format demanding perfect recall.
From Can Bölük's harness benchmark across 16 models:
str_replacefailure rate: up to 50.7% on some models- Root cause: models can't reliably reproduce exact whitespace
When Claude reads a file via linehash read, every line gets a stable 2-char hash:
1:a3| function verifyToken(token) {
2:f1| const decoded = jwt.verify(token, process.env.SECRET)
3:0e| if (!decoded.exp) throw new TokenError('missing expiry')
4:9c| return decoded
5:b2| }
When Claude edits, it references hashes as anchors:
# Replace a single line
linehash edit src/auth.js 2:f1 " const decoded = jwt.verify(token, env.SECRET)"
# Replace a range
linehash edit src/auth.js 2:f1..4:9c " return jwt.verify(token, env.SECRET)"
# Insert after a line
linehash insert src/auth.js 3:0e " if (!decoded.iat) throw new TokenError('missing iat')"
# Delete a line
linehash delete src/auth.js 3:0eIf the file changed since last read, hashes won't match → edit rejected before corruption.
| str_replace | linehash | |
|---|---|---|
| Model must reproduce whitespace | ✅ required | ❌ not needed |
| Stable after file changes | ❌ line numbers shift | ✅ hash tied to content |
| Edit failure rate | Up to 50% | Near 0% |
| Detects stale reads | ❌ | ✅ hash mismatch = reject |
| Token cost | High (full old content) | Low (just hash + new line) |
Each hash is a 2-char truncated xxHash of the raw line content:
line content → xxhash32 → take low byte as 2 hex chars
" return decoded" → 0x...9c → "9c"
- Same content = same hash (stable across reads)
- Different content = different hash (edit safety)
- 2 chars = 256 possible values — good enough for line-level anchoring
- Collisions are rare and recoverable (linehash detects ambiguity)
| Crate | Purpose |
|---|---|
xxhash-rust |
Fast content hashing per line |
clap |
CLI |
serde_json |
--json output for scripts |
Pure Rust. No tree-sitter. No LLM. No external dependencies. Simplest tool in the suite.
Install the latest release with the generated installer:
curl -fsSL "https://raw.githubusercontent.com/quangdang46/linehash/main/install.sh?$(date +%s)" | bashThe installer downloads the matching GitHub release asset for your platform, verifies checksums when available, and can optionally add the install directory to your shell PATH.
cargo install --path crates/coreCommon workflows for Claude Code, AI code editing, and patch-safe file automation:
# Read file with hash tags
linehash read src/auth.js
# Read just the neighborhood around one or more anchors
linehash read src/auth.js --anchor 2:f1 --context 2
# View just line numbers + hashes (no content) — for orientation
linehash index src/auth.js
# Check whether one or more anchors still resolve
linehash verify src/auth.js 2:f1 4:9c
# Search content and return anchors for matching lines
linehash grep src/auth.js "verifyToken"
linehash annotate src/auth.js "missing expiry"
linehash annotate src/auth.js "^export function" --regex --expect-one
# Edit by hash anchor
linehash edit <file> <hash-or-line:hash> <new_content>
linehash edit <file> <start-line:hash>..<end-line:hash> <new_content>
linehash insert <file> <hash-or-line:hash> <new_line> # insert AFTER anchor line
linehash insert <file> <hash-or-line:hash> <new_line> --before
linehash delete <file> <hash-or-line:hash>
# Structural mutations
linehash swap <file> <anchor-a> <anchor-b>
linehash move <file> <anchor> before <target-anchor>
linehash move <file> <anchor> after <target-anchor>
linehash indent <file> <start-line:hash>..<end-line:hash> +2
linehash find-block <file> <anchor>
# Multi-op workflows
linehash patch <file> <patch.json>
# patch.json shape:
# {"ops":[{"op":"edit","anchor":"3:64","content":" return message.toUpperCase()"}]}
linehash from-diff <file> <diff.patch>
linehash merge-patches <patch-a.json> <patch-b.json> --base <file>
# Inspect collision/token-budget guidance for large files
linehash stats src/auth.js
# Watch for live hash changes (v1 defaults to one change event, then exit)
linehash watch src/auth.js
linehash watch src/auth.js --continuous
# Explode / implode workflow
linehash explode src/auth.js --out out/auth.lines
linehash implode out/auth.lines --out src/auth.js --dry-runAdd to your project's CLAUDE.md:
## File Editing Rules
When editing an existing file with linehash:
1. Read: `linehash read <file>`
2. Copy the anchor as `line:hash` (for example `2:f1`) — do not include the trailing `|`
3. Edit using the anchor only; never reproduce old content just to locate the line
4. If the file may have changed, prefer `linehash read <file> --json` first and carry `mtime` / `inode` into mutation commands with `--expect-mtime` / `--expect-inode`
5. If an edit is rejected as stale or ambiguous, re-read and retry with a fresh qualified anchor
Example:
linehash read src/auth.js
# line 2 shows as `2:f1| const decoded = ...`
linehash edit src/auth.js 2:f1 " const decoded = jwt.verify(token, env.SECRET)"- Use
readfor the full file view. - Use
read --anchor ... --context Nwhen you already know the target anchor and want a smaller local window. - Use
indexfor fast orientation when content is not needed. - Use
verifyto confirm anchors still resolve before building a larger edit plan. - Use
grep/annotatewhen you know content but need current anchors. - Use
swap,move,indent, andfind-blockinstead of simulating structural edits with multiple fragile single-line operations. - Use
patch,from-diff, andmerge-patchesfor multi-step or reviewable change sets. - Use
statswhen a file is large, collisions are likely, or you want guidance on whether short hashes and small context windows are still ergonomic. - Use
explode/implodeonly when you explicitly want a filesystem-native round-trip workflow. - Use qualified anchors like
12:abwhenever possible; they are safer than bareabwhen collisions or stale reads matter.
# Pretty (default) — for Claude to read
linehash read src/auth.js
1:a3| function verifyToken(token) {
2:f1| const decoded = jwt.verify(token, SECRET)
...
# JSON — for scripts and stale-guard workflows
linehash read src/auth.js --json
{
"file": "src/auth.js",
"newline": "lf",
"trailing_newline": true,
"mtime": 1714001321,
"mtime_nanos": 0,
"inode": 12345,
"lines": [
{ "n": 1, "hash": "a3", "content": "function verifyToken(token) {" },
{ "n": 2, "hash": "f1", "content": " const decoded = jwt.verify(token, SECRET)" },
...
]
}
# NDJSON event stream for agents / scripts
linehash watch src/auth.js --json
{"timestamp":1714001321,"event":"changed","path":"src/auth.js","changes":[...],"total_lines":847}verifychecks whether anchors still resolve and returns a non-zero exit code if any do not.grepsearches by regex and returns anchor-addressed matches.annotatemaps exact substrings or regex matches back to current anchors.patchapplies a JSON patch transaction atomically.swapexchanges two lines in one snapshot-safe operation.moverepositions one line before or after another anchor.indentindents or dedents an anchor-qualified range.find-blockdiscovers a likely structural block around an anchor.from-diffcompiles a unified diff into linehash patch JSON.merge-patchesmerges two patch files and reports conflicts.explodewrites one file per source line plus metadata.implodevalidates and reassembles an exploded directory back into a file.
# Hash not found
linehash edit src/auth.js xx "new content"
Error: hash 'xx' not found in src/auth.js
Hint: run `linehash read <file>` to get current hashes
# Ambiguous hash (collision)
linehash edit src/auth.js f1 "new content"
Error: hash 'f1' matches 3 lines in src/auth.js (lines 2, 14, 67)
Hint: use a line-qualified hash like '2:f1' to disambiguate
# File changed since read (stale qualified anchor)
linehash edit src/auth.js 2:f1 "new content"
Error: line 2 content changed since last read in src/auth.js (expected hash f1, got 3a)
Hint: re-read the file with `linehash read <file>` and retry the edit
# File metadata changed since JSON read / guard capture
linehash edit src/auth.js 2:f1 "new content" --expect-mtime 1714001321 --expect-inode 12345
Error: file 'src/auth.js' changed since the last read
Hint: re-read the file metadata and retry with fresh --expect-mtime/--expect-inode values- Stale anchor: re-run
linehash read <file>orlinehash read <file> --json; if the error reports relocated line(s), use those to rebuild a fresh qualified anchor before retrying. - Ambiguous hash: switch from bare
abto qualified12:ab. - Large file / too much output: use
index,stats, orread --anchor ... --context Ninstead of a full read. - Concurrent edits: treat a stale-anchor or stale-file rejection as success of the safety system, not as something to bypass.
-
linehash diff— show pending edits before applying -
linehash undo— revert last edit - Multi-line insert block support
- Integration test suite against real codebases