Hashline
Read and edit files using content-addressed hashes instead of plain line numbers. Each line is identified by a combined lineNumber#hash reference, so edits fail predictably when the file has changed since you last read it.
All endpoints require session-based authentication through NextAuth. File paths must resolve to a location within the project directory.
Read file with hashes
GET /api/hashline?path=/src/index.ts
Returns every line of a file annotated with its content hash.
Query parameters
| Parameter | Type | Required | Default | Description |
|---|
path | string | Yes | — | Filesystem path to the file |
format | string | No | json | Response format: json or cli |
{
"path": "/src/index.ts",
"stats": {
"totalLines": 42,
"blankLines": 5,
"uniqueHashes": 38,
"hashCollisions": 2
},
"lines": [
{
"lineNumber": 1,
"hash": "A3",
"content": "import { x } from 'y'",
"isBlank": false
}
],
"formatted": " 1#A3| import { x } from 'y'\n 2#B7| ..."
}
| Field | Type | Description | |
|---|
path | string | The requested file path | |
stats.totalLines | number | Total number of lines in the file | |
stats.blankLines | number | Number of blank lines | |
stats.uniqueHashes | number | Number of distinct hash values | |
stats.hashCollisions | number | Number of lines that share a hash with another line | |
lines | array | Array of line objects | |
lines[].lineNumber | number | 1-indexed line number | |
lines[].hash | string | Short content hash for this line | |
lines[].content | string | Text content of the line | |
lines[].isBlank | boolean | Whether the line is empty or whitespace-only | |
formatted | string | Pre-formatted output with the pattern `lineNumber#hash | content` |
When format=cli, the response is plain text with Content-Type: text/plain. Each line is formatted as:
1#A3| import { x } from 'y'
2#B7| const config = {}
Errors
| Code | Description |
|---|
| 400 | path parameter required — missing path query parameter |
| 401 | Unauthorized — no valid session |
| 403 | Invalid path: must be within project directory — path resolves outside the project root |
| 500 | File read failure (for example, file does not exist) |
Apply an edit
Edit one or more lines by hash reference. If the hash no longer matches the current file content, the request fails with a 409 and suggests similar lines.
Request body (single edit)
| Field | Type | Required | Default | Description |
|---|
path | string | Yes | — | File path to edit |
hashRef | string | Yes | — | Hash reference in the format lineNumber#hash or #hash |
newContent | string | Yes | — | Replacement content for the matched line |
backup | boolean | No | true | Create a timestamped backup before editing |
{
"path": "/src/index.ts",
"hashRef": "12#A3",
"newContent": "import { z } from 'y'"
}
Request body (batch edit)
| Field | Type | Required | Default | Description |
|---|
path | string | Yes | — | File path to edit |
edits | array | Yes | — | Array of edit objects |
edits[].hashRef | string | Yes | — | Hash reference for the line to edit |
edits[].newContent | string | Yes | — | Replacement content |
backup | boolean | No | true | Create a timestamped backup before editing |
{
"path": "/src/index.ts",
"edits": [
{ "hashRef": "12#A3", "newContent": "import { z } from 'y'" },
{ "hashRef": "15#B7", "newContent": "const x = 10" }
]
}
Response (single edit)
{
"success": true,
"path": "/src/index.ts",
"edit": {
"success": true,
"lineNumber": 12,
"oldContent": "import { x } from 'y'",
"newContent": "import { z } from 'y'"
}
}
Response (batch edit)
{
"success": true,
"path": "/src/index.ts",
"results": [
{ "success": true, "lineNumber": 12, "newContent": "import { z } from 'y'" },
{ "success": true, "lineNumber": 15, "newContent": "const x = 10" }
]
}
The top-level success is true only when every edit in the batch succeeds.
Stale line recovery
When a hash reference does not match any line in the current file, the API returns a 409 with suggestions:
{
"error": "Line 12 hash A3 does not match current content",
"suggestion": "Similar lines found:",
"similarLines": [
{ "lineNumber": 5, "hash": "B7", "content": "import { x } from 'z'" }
]
}
Up to 5 similar lines are returned. Re-read the file with GET /api/hashline and retry with an updated hash reference.
Errors
| Code | Description |
|---|
| 400 | path required — missing path in request body |
| 400 | hashRef and newContent required — single edit mode with missing fields |
| 401 | Unauthorized — no valid session |
| 403 | Invalid path: must be within project directory — path traversal attempt |
| 409 | Stale line — the hash reference does not match the current file. Response includes similarLines when available. |
| 500 | Edit failed for a reason other than a stale reference |
Delete a backup
DELETE /api/hashline?path=/src/index.ts.backup.1712000000
Remove a backup file created by a previous edit. Only files containing .backup. in their name can be deleted through this endpoint.
Query parameters
| Parameter | Type | Required | Description |
|---|
path | string | Yes | Path to the backup file. Must contain .backup. in the name. |
Response
{
"success": true,
"message": "Deleted: /src/index.ts.backup.1712000000"
}
Errors
| Code | Description |
|---|
| 400 | path parameter required — missing path query parameter |
| 401 | Unauthorized — no valid session |
| 403 | Can only delete .backup. files — path does not contain .backup. |
| 403 | Invalid path — path resolves outside the project directory |
| 500 | Delete failed (for example, file does not exist) |
A hash reference combines a line number and a short content hash:
For example, 12#A3 refers to line 12 with hash A3. You can also use #A3 without the line number, though including the line number improves match accuracy when hashes collide.
The hash is derived from the trimmed content of the line. Two lines with identical content after trimming share the same hash. The stats.hashCollisions field in the read response tells you how many lines share a hash.