TL;DR: A code generator that scaffolds your project is lovely on day one and a menace on day ninety, because the day you ask it to regenerate is the day it flattens every change you made. go-tool-base’s generator records a SHA-256 of every file it writes into a manifest. On regeneration it re-hashes each file first: matches the stored hash, it was never touched, overwrite freely. Doesn’t match, you’ve edited it, so it stops and asks. A .gtb/ignore file lets you mark files permanently hands-off. The result is a scaffold you can keep using instead of one you outgrow in a week.

The generator’s awkward second act

A project generator has an easy first act. gtb generate skeleton, and you’ve got a complete, wired, idiomatic Go CLI project. Everyone’s happy.

The second act is the hard one. The framework moves on. A convention changes, a new built-in capability appears, the recommended CI shape shifts. Your project, scaffolded three months ago, is now subtly out of date, and you’d quite like the generator to bring it back up to spec.

Except by now it isn’t a fresh scaffold. It’s your project. You tuned the CI workflow. You rewrote the justfile. You added a stanza to the Dockerfile that took an afternoon and a lot of swearing to get right. The generated files and your edited files are the same files.

A naive generator handles this with breathtaking confidence: it regenerates everything from the template and overwrites the lot. Run it once, lose your afternoon. You learn the lesson immediately and never run regeneration again, which means the upkeep feature you were sold is dead on arrival. A scaffold you can’t safely re-run is just a one-shot cp.

What the generator needs to know

The thing standing between “safe to overwrite” and “absolutely do not” is a single fact: has this file changed since the generator last wrote it?

If it hasn’t, the file is still pristine boilerplate and the generator owns it. Overwrite away. If it has, a human has been in there, and the generator must not touch it without asking.

The generator can’t eyeball that. It needs a record. So every time gtb generate writes a file, it computes a SHA-256 of the content and stores it in the project’s manifest — .gtb/manifest.yaml, a Hashes map of relative path to hash. The manifest is the generator’s memory of the exact bytes it last produced.

Regeneration becomes a three-way decision

With that record in hand, regeneration stops being “overwrite everything” and becomes a per-file decision with three branches.

The file doesn’t exist. Easy. Write it, store its hash.

The file exists and its current hash matches the manifest. It is byte-for-byte what the generator last wrote. Nobody has touched it. The generator owns it outright, so it regenerates from the template and updates the stored hash. No prompt, no fuss. This is the common case, and it’s silent precisely because it’s safe.

The file exists and its hash does not match. Someone has edited it since generation. The generator stops and asks. It will not silently overwrite your afternoon. You decide: take the new version, or keep yours.

The detail I’m fond of is what happens when you decline. Declining is non-fatal. Generation carries on with the rest of the files, and the manifest keeps the file’s stored hash rather than dropping it. That matters, because it means the file stays tracked. The next time you regenerate, the generator can still tell that file has been modified, and still asks. Skipping a file once doesn’t quietly evict it from the generator’s awareness forever. It stays a known, watched, customised file across every future run.

When you want it to stop asking

Per-file prompting is the right default, but for files you have permanently taken ownership of, being asked on every single regeneration is just noise. If you’ve rewritten the CI workflows wholesale and you are never going back to the generated version, you don’t want a prompt, you want the generator to leave them alone and not mention it.

That’s what .gtb/ignore is for. It sits next to the manifest and takes gitignore-style patterns:

# I own the CI workflows now
.github/workflows/**

# ...except the release workflow, keep that managed
!.github/workflows/release.yml

# and my build config
justfile
Dockerfile

Anything matching is skipped during regeneration with no prompt at all. Patterns evaluate top to bottom and later ones win, so the negation (!) works the way you’d expect from .gitignore: exclude a whole directory, then claw one file back.

It’s a deliberate escalation ladder. Unmodified files are handled silently. Modified files get a prompt. Files you’ve formally claimed get total silence. Each rung asks for less of your attention than the last, and you choose how far up to climb per file.

Summary

A generator earns its keep twice: once when it scaffolds your project, and then continuously, every time it pulls that project back up to the framework’s current shape. The second job is worthless if regeneration flattens your customisations, because you’ll simply stop running it.

go-tool-base’s generator avoids that by remembering. It hashes every file it writes into .gtb/manifest.yaml, and on regeneration it re-hashes before overwriting: unchanged files it owns and updates silently, changed files it stops and asks about, and .gtb/ignore lets you mark files as permanently yours. Skipped files stay tracked, so the generator never loses sight of what you’ve made your own.

The point of a scaffold isn’t the first five minutes. It’s that you can still run it in month three without holding your breath.