axm-edit
Atomic batch file editing for AI agents.
Overview
IDE agents edit files one-at-a-time. A refactor touching 30 files = 30 tool calls. The agent spends 70% of its budget on mechanics.
axm-edit replaces all of that with 1 call.
Features
- 🔧
batch_edit— Replace, rewrite, create, and delete files in a single atomic operation - 🧪
batch_edit_check— Read-only preflight of a batch: diagnostics without touching the disk - 🔬
file_bytes— Read-only byte-level report on a file: sha256, literal non-ASCII vs textual escape sequences - 📖
read_file— Read file content with optional line-range support - 🔍
search_files— Grep-like search across project files (literal or regex) - 📂
list_dir— List files and directories with metadata (recursive, depth-limited) - ✏️
write_file— Write or overwrite file content - 🖊️
edit_file— Find-and-replace (substring) in a single file - ▶️
run_command— Execute shell commands with timeout and output truncation - ⏪
batch_rollback— Restore the exact paths a batch touched from a targeted snapshot - 🛡️ Atomic — All-or-nothing: validation runs before any file is touched
- 📐 Bottom-to-top — Line edits applied in reverse order to avoid line-shift problems
- 🔒 Safe — Path traversal blocked,
oldcontent validated, targeted path snapshot before writes
Installation
uv add axm-edit
Or as a workspace dependency in pyproject.toml:
[project]
dependencies = ["axm-edit"]
[tool.uv.sources]
axm-edit = { workspace = true }
Quick Start
from axm_edit.core.engine import batch_apply
from axm_edit.models.operations import Edit, ReplaceOp, CreateOp
from pathlib import Path
result = batch_apply(
root=Path("/my/project"),
operations=[
ReplaceOp(file="src/core.py", edits=[
Edit(line=5, old="class OldName:", new="class NewName:"),
]),
CreateOp(file="src/new.py", content='"""New module."""\n'),
],
)
print(result.summary) # {"modified": 1, "created": 1, "deleted": 0}
MCP Tools
batch_edit
Atomic batch file operations.
| Param | Type | Default | Description |
|---|---|---|---|
path |
str |
"." |
Project root directory |
operations |
list[Op] |
— | List of replace/create/delete/rewrite operations |
lint |
bool |
True |
Run ruff --fix on changed Python files after apply |
lint_diff |
bool |
True |
Surface per-file lint_diffs hunks of post-lint mutations |
lint_diff_max_ratio |
float |
0.5 |
Fallback to file_reread_recommended when len(diff) > ratio * len(post) |
When lint_diff=True and ruff/harness_fix mutates any Python file, ToolResult.data["lint_diffs"] lists one entry per file: {"file", "rules": [ruff codes], "diff": "@L<n>\n-old\n+new..."}. On large rewrites the entry drops diff and carries "diff_skipped": "file_reread_recommended".
4 operation types:
replace — modify lines in an existing file
{
"op": "replace",
"file": "src/foo.py",
"edits": [
{"line": 3, "old": "import bar", "new": "import baz"},
{"line": 17, "old": "x = bar()", "new": "x = baz()"}
]
}
All line numbers reference the original file. The engine sorts edits bottom-to-top.
create — create a new file
{"op": "create", "file": "src/new.py", "content": "\"\"\"New module.\"\"\"\n"}
Fails if the file already exists — fail-closed, there is no overwrite
flag. To replace an existing file, use rewrite and hand back the digest of
the bytes you read.
rewrite — replace an existing file in full
{
"op": "rewrite",
"file": "src/foo.py",
"content": "\"\"\"Rewritten module.\"\"\"\nvalue = 1\n",
"expected_checksum":
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
Three required keys — file, content and expected_checksum (the preflight
tool batch_edit_check names that same key checksum; batch_edit accepts
either spelling and normalises it). The digest is
mandatory: it is the sha256 hex digest of the file bytes as currently on
disk (read the file, hash it, pass it back). A stale digest is a hard
refusal, so a concurrent modification is never silently clobbered — and, here
too, there is deliberately no overwrite escape hatch.
rewrite carries the exact bytes of content: no anchor resolution, no quote
normalisation, no re-indentation. That makes it the safe way to replace a
triple-quote-heavy module that anchor-based replace cannot address. The
rewritten file joins the post-apply ruff --fix pass like any other touched
Python file.
delete — remove a file
{"op": "delete", "file": "src/old.py"}
batch_edit_check
Read-only preflight of a batch_edit operation set. It runs the same validation
rules as batch_edit (broken old anchors, create on an existing file,
unknown edit keys, path traversal) and reports what would go wrong — read-only:
no disk write is ever performed, not a single file is created, modified or deleted.
| Param | Type | Default | Description |
|---|---|---|---|
path |
str |
"." |
Project root the batch would be applied to |
operations |
list[Op] |
— | Same operation list as batch_edit |
{
"path": "/my/project",
"operations": [
{
"op": "replace",
"file": "src/foo.py",
"edits": [{"line": 3, "old": "import bar", "new": "import baz"}]
},
{"op": "create", "file": "src/new.py", "content": "x = 1\n"}
]
}
Returns data["ok"] (true when nothing is wrong) and data["diagnostics"],
one entry per problem found, emitted in operation order (the batch's own
order, then rule family) — plus the machine-readable severity partition
data["blocking"] (true as soon as one diagnostic is an error),
data["error_count"] and data["warning_count"]. The rendered text ends with
the matching summary line, blocking: yes (N errors, M warnings) or
blocking: no (0 errors, M warnings), so an agent never has to re-parse the
diagnostic lines to know whether the batch would be rejected. Since nothing is
written, it is safe to call it as often as needed before committing to a real
batch_edit.
file_bytes
Byte-exact report on a file already written to disk — read-only: the
file is opened once in binary mode, no disk write is ever performed and no
metadata is changed. path is the absolute path to the file.
| Param | Type | Default | Description |
|---|---|---|---|
path |
str |
— | Absolute path to the file to inspect |
expected |
str? |
None |
Content the caller believes it wrote |
expect_escaped |
bool |
False |
Whether escape sequences are expected verbatim on disk |
encoding |
str |
"utf-8" |
Decoding used for the report (only utf-8) |
Returns data["verdict"] — one of ok, mismatch,
literal_where_escaped_expected, escaped_where_literal_expected,
decode_error — plus data["sha256"], data["size_bytes"], the bounded
occurrence lists with their *_total counters, data["mismatch"] (offset and
ASCII-only reprs of the first divergence) and an actionable data["hint"].
Undecodable bytes are a diagnostic (encoding_ok: false), not an error.
read_file
Read file content with optional line-range support.
| Param | Type | Description |
|---|---|---|
path |
str |
Project root directory |
file |
str |
Relative path to the file |
start_line |
int? |
Optional 1-indexed start line (inclusive) |
end_line |
int? |
Optional 1-indexed end line (inclusive) |
search_files
Grep-like search across project files.
| Param | Type | Description |
|---|---|---|
path |
str |
Project root directory |
pattern |
str |
Search string or regex (required) |
is_regex |
bool? |
Treat pattern as regex (default false) |
include |
list[str]? |
Glob patterns to filter files (e.g. ["*.py"]) |
list_dir
List files and directories with metadata.
| Param | Type | Description |
|---|---|---|
path |
str |
Root directory to list (default ".") |
max_depth |
int? |
Recursion depth — 1 for immediate children only (default 1) |
run_command
Execute shell commands with timeout and output truncation.
| Param | Type | Description |
|---|---|---|
path |
str |
Project root directory |
command |
str |
Shell command string (required) |
cwd |
str? |
Working directory, relative to root |
timeout |
int? |
Timeout in seconds (default 30) |
write_file
Write (create or overwrite) a single file. path is the absolute path
to the file (not a project root); parent directories are created as needed.
| Param | Type | Description |
|---|---|---|
path |
str |
Absolute path to the file to write |
content |
str |
Text content to write |
edit_file
Find-and-replace in a single file. Unlike batch_edit (whole-line old),
edit_file matches old/new as substrings. path is the absolute
path to the file.
| Param | Type | Description |
|---|---|---|
path |
str |
Absolute path to the file to edit |
old |
str |
Text to find (exact substring match) |
new |
str |
Replacement text |
count |
int? |
Max replacements — -1 for all, else a positive integer (default 1) |
Note —
write_fileandedit_filetake an absolute file path and are not confined to a project root, unlike the root-relative tools (read_file,search_files,list_dir,run_command,batch_edit). Likerun_command, they are not a sandbox: only point them at paths you intend to write.
batch_rollback
Restore the exact paths a batch touched from its snapshot.
| Param | Type | Description |
|---|---|---|
path |
str |
Project root directory |
checkpoint |
str |
Snapshot payload (JSON string) from the batch_edit response — pass it back verbatim, it is not a short hash |
Development
This package is part of the axm-forge workspace.
git clone https://github.com/axm-protocols/axm-forge.git
cd axm-forge
uv sync --all-groups
# Run tests for this package
uv run pytest --package axm-edit
License
Apache-2.0 — © 2026 Gabriel Jarry
Metadata
Release files for axm-edit 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| axm_edit-0.3.0.tar.gz | 165.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| axm_edit-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 258.2 kB
Release files / axm_edit-0.3.0.tar.gz
| Download URL | axm_edit-0.3.0.tar.gz |
|---|---|
| Size | 165.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
01e2990cee9ba41716b0ff97912f0d07dda9926dcb11c3339f1f36741ab322a4
|
|
BLAKE2b-256 checksum How to use checksums |
a7ab4e49764bf81b17e9df708b2b74f033eadbe05264dcfe56323f377d34c34b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logRelease files / axm_edit-0.3.0-py3-none-any.whl
| Download URL | axm_edit-0.3.0-py3-none-any.whl |
|---|---|
| Size | 92.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
56f6fe6793025b9f5c74c1e7cb23ad50f29e8606cb235b1fef564206e90a9582
|
|
BLAKE2b-256 checksum How to use checksums |
6f4ab2af3655fe7b339a207a9e5078f07ac46298e4f7b5323660584c12c8f138
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency log