hashline
Hash-anchored file editing with hashline — read a file as line:hash anchors, then patch by anchor so an edit against drifted content is rejected instead of silently landing on the wrong line. Use when
SKILL.mdskills/hashline/SKILL.md
GitHubhashline
Every line carries an xxh32 content hash. You target lines by line:hash
anchors instead of by line number or by matching a string. If the file drifted
since you read it, the hash no longer matches and the patch is rejected — you
never write to the wrong place, and you find out immediately rather than three
edits later.
Requires the hashline binary (developed against 0.9.1) on PATH, or
HASHLINE_BIN pointing at it.
Getting the tools
Any harness with native MCP support (Claude Code, Codex, …) — register the stdio server and call its tools directly:
{ "mcpServers": { "hashline": { "command": "hashline", "args": ["mcp"] } } }
Prime Agent — this skill ships a Python package that speaks the same stdio
server from the IPython kernel. See
references/prime-agent-setup.md for install
and the AGENTS.md rules; once loaded:
import hashline
print(await hashline.read("src/auth.py"))
Every kernel call is async — always await. The rest of this skill applies to
both: same server, same tools, same patch syntax.
Workflow
Always read → patch → verify. The read gives you the anchors; the patch result is the re-read file, so it doubles as the verification.
[src/auth.py#1a2b]
1:4f|def verify_token(token):
2:a3| decoded = jwt.decode(token, SECRET)
3:9c| return decoded
1a2b is the whole-file hash; 2:a3 is the anchor for line 2. Copy anchors
verbatim from the read — never guess a hash.
await hashline.patch("src/auth.py", """SWAP 2:a3:
+ decoded = jwt.decode(token, SECRET, algorithms=["HS256"])""")
Operations
Each op is a line; content lines are prefixed with +. Ranges are N..M
(N.=M also accepted). The :HH hash is optional but always include it —
it is the whole point of the tool.
| Op | Effect |
|---|---|
SWAP N:HH: + +lines |
Replace line N |
SWAP N:HH..M:HH: + +lines |
Replace lines N through M |
DEL N:HH |
Delete line N |
DEL N..M |
Delete lines N through M |
INS.PRE N:HH: + +lines |
Insert before line N |
INS.POST N:HH: + +lines |
Insert after line N |
INS.HEAD: + +lines |
Insert at start of file |
INS.TAIL: + +lines |
Insert at end of file |
SWAP.BLK N:HH: + +lines |
Replace the whole syntactic block around N |
DEL.BLK N:HH |
Delete the syntactic block around N |
INS.BLK.PRE / INS.BLK.POST N:HH: + +lines |
Insert before/after the block around N |
Multiple ops in one patch, wrapped in envelope markers:
*** Begin Patch
SWAP 5:1a:
+ decoded = jwt.decode(token, SECRET, algorithms=["HS256"])
DEL 9:c3
INS.POST 12:d4:
+ log.info("verified %s", decoded["sub"])
*** End Patch
Ops within one patch address the ORIGINAL line numbers — do not re-number for earlier inserts or deletes in the same patch.
Use dry_run=True to validate anchors without writing.
The other tools
| Tool | Arguments |
|---|---|
read |
file, json=False |
patch |
file, patch, dry_run=False |
write |
file, content, force=False (create; force to overwrite) |
find_block |
file, anchor — the structural block around a line:hash |
remove_file |
file |
rename_file |
src, dst |
The parameter is file, not path. Relative paths resolve against the working
directory. list_tools() discovers them at runtime if in doubt.
Rules
- Read before you patch, every time. Anchors from an earlier turn may be stale.
- Never invent an anchor. Copy
line:hashfrom the most recent read output. - Failures come back as text, not exceptions. A rejected patch returns a
string starting with
Error:(e.g.line 2 content changed since last read (expected hash ff, got bf)) and the call still "succeeds". Read the returned text before assuming the edit landed. - A rejected patch means the file changed under you. Re-read and rebuild the patch — do not retry with the hash stripped off.
- Prefer
SWAP.BLKover a longSWAP N..Mwhen replacing a whole function. - Use this instead of string-replace edit tools,
open(...).write(),sed, or shell heredocs. Those bypass the drift check that makes hashline worth using.
Troubleshooting
RuntimeError: hashline binary not found— install hashline, or setHASHLINE_BINto its absolute path.unknown operationin the warnings — the op name or theline:hashis malformed. Re-read the file and rebuild the anchor from its output.