Would you paste a suggested patch onto a server you just met because the reply called the change safe? I would not, and these notes are the refusal I want to keep for the next host. A free chat can draft a tidy edit while the process that would run it still sits in the wrong directory. So I spent this window on a plan file, not on a write I could not undo.
What I tried before I trusted the second host
I started from a boring question that still saves me more time than another pass over the patch. Which paths would change, and which of those paths are even present on both sides? A model can describe an edit in a paragraph, but a paragraph is not a list of operations with roots and intents. I wanted that list from the laptop and from the remote session before either side created a file.
Would a matching pair of path lists be enough, by itself, to apply the change on the second host? Not by itself, because the bytes you write can still be wrong even when the names already agree. A mismatch is already a stop, and that stop is the part I care about recording first. I kept the first pass read-only, and I pointed the script at a fixture tree I was willing to delete.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Free model access is where I would ask for a plainer reading of a confusing plan diff. The free server option is the second host this checklist expects, when that option is actually offered. I am not claiming a quota, a hardware shape, a duration, or a promise that those options stay fixed.
If either option is missing on the account you use, the same script still earns its keep on two local directories. I would not invent a stand-in host and then describe that folder as if it were the offered server. A missing option is a missing option, and the checklist should say so instead of papering over the gap. The comparison habit does not require a product name in order to be worth running.
What broke once the plans could disagree
The first break was social, not technical, and it showed up inside the suggested shell block. The reply bundled the plan command and the apply command in one fenced region, so a hurried paste would write first. Have you ever copied a quick check that ended in a mutation you did not mean to run? That is the failure the confirm flag is there to catch, and I now refuse a block that mixes both steps.
The second break was the current directory, which is a quieter way to aim at the wrong tree. A relative root of . meant the repo on the laptop and another folder on the server. The plan then listed different parents for the same basename, which is an easy miss in a summary. I stopped accepting a relative root unless both plans print the resolved path and those strings agree.
A third break appeared when a scratch file existed only on the server side of the rehearsal. The plan grew an operation that the laptop never proposed, and a summary might call that extra harmless. I do not treat extra names as harmless, because an apply that trusts the longer list writes something I never reviewed. Sorting became the fourth break, and it was the one I nearly blamed on the suggestion.
A locale-aware sort reordered names that differed only by case, and the textual diff looked like a logic bug. I pinned the sort to raw code points, and I stopped blaming the suggestion for a difference the printer invented. Would I have noticed that inside a chat summary that called the plans basically the same? Probably not, because a summary flattens order and it drops the resolved paths I needed.
The prefix check I would not ship
I almost treated a string prefix test as proof that a path stayed inside the root. That check is wrong when one directory name is a prefix of a neighbor directory beside it. /tmp/app is a prefix of /tmp/app-backup, so a string test would allow a walk into the neighbor. The script below uses Path.relative_to after resolve, and I would not ship the string version as a shortcut.
Does that distinction matter on a short home path that looks obvious in a prompt? It matters wherever two directories share a prefix, which is a common layout under temporary folders and under home. I would rather fail the plan than explain a write that landed next door. A guard that only works on pretty paths is not a guard I want on a second host.
The artifact I would run again
This is a small standard-library script, not a framework, and not a stand-in for a deploy system. It builds a plan, compares that plan with a file you already saved, and applies only with a separate confirm flag. Treat the listing as a workflow you can execute on a disposable tree you already understand. I am not attaching timings, quotas, hardware claims, or a vendor scoreboard to this script.
#!/usr/bin/env python3
'''plan_guard.py — emit a read-only plan, then require a separate apply.
Apply writes a placeholder only. Point --root at a tree you can delete.
Type hints assume Python 3.9 or newer.
'''
import argparse
import json
import sys
from pathlib import Path
def build_plan(root: Path, names: list[str]) -> dict:
root = root.resolve()
ops = []
for name in names:
rel = Path(name)
if rel.is_absolute() or any(part == '..' for part in rel.parts):
raise SystemExit(f'refusing unsafe name: {name}')
path = (root / rel).resolve()
try:
path.relative_to(root)
except ValueError:
raise SystemExit(f'refusing path outside root: {name}')
if path.exists() and not path.is_file():
raise SystemExit(f'refusing non-file: {path}')
ops.append({
'op': 'write',
'rel': name,
'resolved': str(path),
'existed': path.exists(),
})
ops.sort(key=lambda item: item['rel'])
return {'root': str(root), 'ops': ops}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--root', required=True)
parser.add_argument('--name', action='append', required=True)
parser.add_argument('--apply', action='store_true')
parser.add_argument('--confirm', default='')
parser.add_argument('--plan', default='')
args = parser.parse_args()
plan = build_plan(Path(args.root), args.name)
if not args.apply:
sys.stdout.write(json.dumps(plan, indent=2, sort_keys=True) + '\n')
return 0
if args.confirm != 'yes' or not args.plan:
print('apply refused: need --confirm yes and --plan', file=sys.stderr)
return 2
reviewed = json.loads(Path(args.plan).read_text(encoding='utf-8'))
if reviewed != plan:
print('apply refused: reviewed plan != live plan', file=sys.stderr)
return 3
for op in plan['ops']:
dest = Path(op['resolved'])
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text('placeholder\n', encoding='utf-8')
print('applied %s write(s)' % len(plan['ops']))
return 0
if __name__ == '__main__':
raise SystemExit(main())
Commands I keep beside the note
I run the plan locally, then I run the same names on the second host, and only then do I consider apply. I save both JSON files, and I diff them with a tool that does not reorder lines for me. The second host can be the free server option, when that option is the one you actually have. It can also be another folder on the same laptop if that option is not in front of you today.
mkdir -p ./fixture/notes
python3 plan_guard.py --root ./fixture --name notes/day1.txt --name notes/day2.txt > local-plan.json
python3 -m json.tool local-plan.json > /dev/null
diff -u local-plan.json remote-plan.json
python3 plan_guard.py --root ./fixture --name notes/day1.txt --name notes/day2.txt --apply --confirm yes --plan local-plan.json
I never feed the laptop plan into an apply on the server, because the resolved paths will differ and the guard will refuse. The cross-host step is a reading of rel and existed, not a blind replay of the other file. Apply only replays the plan that the same host just printed, after you have accepted that reading. If you skip that distinction, the confirm flag becomes a way to fail, not a way to write.
Checks I want red before I trust a green run
A single happy path is how a guard rots, so I keep a short list of failures I expect to see. Each one should fail closed, with a non-zero status, before I believe the success case. I would rather rehearse these five than collect a story about a clean run. None of them require a product setting, a quota, or a named model.
- Run apply with no confirm flag, and expect exit code 2 plus a refusal line on stderr.
- Save a plan, add one more
--name, and expect exit code 3 because the reviewed file no longer matches. - Pass a
--namethat climbs out with.., and expect the unsafe-name refusal before any write occurs. - Point
--rootat two different directories, and confirm thatresolveddiffers even whenrelstill matches. - Create a directory where a named file should be, and expect the non-file refusal instead of a write.
The decision table I actually look at
I do not want a vibe check after a reply that sounds confident about a tree it has not listed. I want a row that says stop, rerun, or apply, and I want that row to name evidence I can open in an editor. Would I skip a row because the suggestion already apologized for the mismatch? I would not, because an apology is not a diff.
- If the
rootstrings differ, I stop, and I fix the working directory before I discuss the patch. - If one plan has an extra
rel, I stop, and I ask which host invented that name. - If
existeddiffers, I rerun after I decide whether create and overwrite carry the same risk here. - If the diff is empty and the tree is disposable, I may apply, and I still keep the plan file next to the log.
- If the model says the diff is empty but the file diff is not, I trust the file and I reread the command.
What I would repeat on the second day
I would repeat the split between asking and applying, because that split is the whole habit. Free model access helps when I want a second reading of a confusing diff, or a plainer description of the schema. It does not help as a witness that the server tree matched the laptop tree. It cannot see that tree unless I paste the plan, and a paste can quietly drop a path.
Would I let the reply set the confirm flag for me after a clean-sounding summary on the second host? I would not, and the flag stays in the command I type only after the diff is on screen. The model can explain a mismatch, and it cannot close the mismatch by sounding sure. That line is the one I would copy into the next note if I copied nothing else.
I would also repeat the hostile fixture, even on the second day, when the first run looks clean. Each rehearsal gets one extra name, one missing name, and one run from the wrong directory. Those three failures are cheap, and they train the stop better than another clean success does. I would keep the server session on a tree I can delete, even when the option is free.
Free does not mean those files are isolated from other work sitting in the same home. A shared home directory is still a shared home directory, and a placeholder write is still a write you must intend. I would repeat this same note format on the second day, including the temptation I almost hid. The first day records the commands I ran and the mismatch that surprised me in the diff.
The second day records whether I wanted to skip the diff because the suggestion sounded sure of itself. That temptation is the bug I am tracking, and the script is only the fence I put around it. If the fence feels annoying during a quiet rehearsal, it is still doing the job I wrote it down for. I would rather repeat that annoyance than repeat a write I cannot explain.
Limits, and who should skip this
This guard does not review the bytes you meant to write, and the sample apply stores a placeholder rather than your real patch. It will not catch a bad transformation that happens to touch the same paths you already allowed. It will not protect you if you remove the confirm check, or if your user can write outside the root. A mislabeled shell is also outside its reach, because the script cannot tell which host you think you are on.
You should skip this approach if you need a signed release, a review of binary files, or proof that two machines share a full source tree. You should also skip it if you cannot stand a manual diff, because the method is that diff and the script will not perform it for you. Teams that already have a dry run inside their deploy tool do not need a second hobby script that can drift from that tool. If the free server option is unavailable, do not invent a substitute host and then describe the rehearsal as the same place.
The example assumes a POSIX-like path style, and I would not treat a green run here as a Windows proof. After resolve, a symlink that leaves the root is refused, which is what I want for this guard, and it is not a general symlink policy. The allowlist has to be names you typed, not a generated glob you did not read line by line. If any of those limits collide with the job in front of you, pick the stricter process you already trust.
If those two options are available on the account you already use, rehearse the plan on the free server. Keep the apply on a tree you can throw away, even after that rehearsal looks quiet. Ask the free model for a plain reading of the mismatch, then decide from the files rather than from the tone. Two local folders still prove the checklist if you would rather leave the product out of the loop.
Top comments (0)