Letting a Model Write Files Without Losing the Plot


Up to now kodr could read your workspace, pack it into context, and parse JSON back out of the model’s reply. What it could not do was touch a single file. That was on purpose. Phase 08 of kodr is where writes finally happen - and the whole phase is really about not trusting them.

Because here is the uncomfortable bit: a file path from a model is just text from an untrusted source. Even after phase 05 hands you valid JSON, “valid” only means it parsed. It does not mean the path is src/app.mjs and not ../../.ssh/authorized_keys.

The path jail

So before anything gets written, the path has to clear a jail:

  • No absolute paths. A write target is relative to the workspace or it is rejected.
  • No .. segments. No climbing out the back door.
  • No symlink escapes. It rejects symlinked parents and existing symlink targets, because a symlink is just a .. you cannot see in the string. The check resolves the real path of the parent and confirms it still lives inside the workspace root.

If the resolved parent lands outside the root, you get a SafeWriteError and nothing happens. Untrusted text does not get to pick where bytes land.

Dry run by default

The default is to not write. A dry run produces a simple before/after diff for every proposed change so I can read exactly what the model wants to do before it does it. Same theme as every phase before it: see the input - and now the output - before committing to it.

Flip to apply mode and it actually writes, but it does not throw the old content away. Existing files get copied to a timestamped backup under .kodr/backups/ first, then overwritten.

One thing I was deliberately honest about in the phase notes: these are controlled writes with backups, not full rollback transactions. If a batch fails halfway, earlier files are already written (their backups exist, but there’s no automatic unwind). Calling it a “transaction” would have oversold it. Backups give you a manual escape hatch; they do not give you atomicity. Worth knowing the difference before you lean on it.

Still not wired in

Note what this phase pointedly does not do: connect to the model. safe-writes.mjs is a standalone module with its own tests for path escapes, symlink escapes, and the dry-run/apply split. Proposal flow - actually feeding model output into this gate - is a later phase. Build the gate, prove the gate, then open it.

Links: