Human Approval Before an AI Agent Writes to Your Filesystem
An autonomous coding or file-organizing agent with write access can overwrite the wrong file in one bad tool call — route every write through a human approval gate first, with the diff attached, so nothing lands unseen.
Not all writes carry the same risk
| Write type | Blast radius if wrong | Gate it? |
|---|---|---|
| New file in a scratch/output directory | Low — easy to delete | Usually no |
| Edit to an existing tracked source file | Medium — overwrites working code | Yes |
Write to a config file, .env, or credentials-adjacent path |
High — can break auth, deploys, secrets handling | Always |
| Delete or truncate an existing file | High — often unrecoverable without VCS | Always |
| Write outside the intended project root (path traversal, wrong cwd) | High — unpredictable | Always |
A reasonable default: gate any write that isn't a brand-new file inside a directory the agent was explicitly told is disposable. Everything else goes through approval.
The integration
This example is a Python agent with a write_file tool. Instead of writing immediately, it stages the change, computes a diff against the current file (if one exists), and pushes that diff as the approval preview.
import difflib
import os
import time
import requests
IMPRI_KEY = os.environ["IMPRI_API_KEY"]
BASE = "https://api.impri.dev"
def request_write_approval(path: str, new_content: str) -> str:
old_content = ""
if os.path.exists(path):
with open(path) as f:
old_content = f.read()
diff = "".join(difflib.unified_diff(
old_content.splitlines(keepends=True),
new_content.splitlines(keepends=True),
fromfile=path, tofile=path,
)) or "(new file, no prior content)"
resp = requests.post(f"{BASE}/v1/actions", headers={
"Authorization": f"Bearer {IMPRI_KEY}",
"Content-Type": "application/json",
}, json={
"kind": "fs.write",
"title": f"Write to {path}",
"preview": {"format": "markdown", "body": f"```diff\n{diff}\n```"},
"expires_in": 1800,
"editable": ["preview.body"],
"idempotent": False,
"undo": f"Restore {path} from git or the previous version shown in this diff",
})
return resp.json()["id"]
def await_and_write(path: str, action_id: str) -> str:
while True:
resp = requests.get(f"{BASE}/v1/actions/{action_id}",
headers={"Authorization": f"Bearer {IMPRI_KEY}"})
data = resp.json()
if data["status"] == "pending":
time.sleep(5)
continue
if data["status"] != "approved":
return data["status"] # rejected or expired — caller does not write
final_body = data["decision"]["final_preview"]["body"]
# final_body is the diff, possibly edited — apply it, or re-derive content
# from the agent's own state if the human only approved without editing.
with open(path, "w") as f:
f.write(new_content_from(final_body))
requests.post(f"{BASE}/v1/actions/{action_id}/result",
headers={"Authorization": f"Bearer {IMPRI_KEY}"},
json={"status": "executed", "payload": {"path": path}})
return "executed"Because editable includes preview.body, a reviewer can hand-edit the diff text in the inbox before approving — useful for trimming an over-eager rewrite down to the lines that actually need to change. If your agent can't safely reconstruct a file from an edited diff, drop editable for this action kind and treat the approval as yes/no only; the tradeoff is real and worth making deliberately rather than defaulting to "editable everywhere."
Why idempotent: false and undo matter here specifically
File writes are usually not idempotent — writing the same content twice is harmless, but retrying after a partial failure risks writing over a write that already partially landed. Setting "idempotent": false puts a warning badge on the card so the reviewer knows a retry isn't automatically safe. The undo field matters more for filesystem actions than almost any other kind: unlike an email you can't unsend, a file write in a git-tracked repo genuinely can be undone, but only if the agent tells the reviewer how before they approve, not after something goes wrong.
Making the gate the only path to disk
This pattern only holds if write_file above — not the agent's model output — is the sole function with filesystem write access in that process. If the agent is also running inside an environment where it can shell out (os.system, a code-execution tool, a raw MCP filesystem server with no wrapper), it can write around the gate entirely. Wrap or disable direct filesystem tools and expose only the approval-gated write_file; the SDK integration patterns cover this kind of tool substitution in more depth. If your agent runs on the Claude Agent SDK, this is the same principle as restricting the Edit/Write tool to a checked wrapper rather than the raw filesystem.
What Impri does not check
Impri does not know whether the new file content is syntactically valid, whether it passes your linter, or whether it will actually build. It shows the human a diff and holds a decision — nothing more. Pair this with your existing CI and pre-commit checks, which should still run on whatever the agent eventually writes; approval and correctness-checking are separate concerns that happen to share a gate here.
Next: the quickstart covers getting an API key for cloud or self-hosted use, and the Python SDK wraps the raw requests calls above into a small client if you'd rather not hand-roll the polling loop.