Sooner or later, in every security review, someone leans back and asks the question:
“Can we fork it?”
Yes, you can fork dedo. Since v0.24.0 there is a guide and a script for it, and a fork takes upstream releases with one merge and one command.
You almost certainly don’t need to. This post is mostly about why not. The rest is for the organisations whose policy says otherwise, so that for them a fork is a little boring, and that’s the goal.
The usual reasons, and why they don’t hold
People reach for a fork for a handful of reasons. Most of them are already covered.
“We have our own conventions.” Wonderful. So does everyone. That’s what house rules are for. Your rules, your agent annexure, your overrides of dedo’s rules and the rules you turn off all live in dedo.yaml, in your repositories, not in dedo. A fork to change a rule is like forking Go to change your naming conventions.
“What if someone changes what we installed?” They can’t. Every Go module is pinned by go.sum and recorded in the public checksum database, an append-only log that nobody can rewrite, the author included. buf.lock pins the options module’s digest. House rules and annexures are pinned by SHA-256. The bytes you installed are the bytes you get.
“What if upstream disappears?” Then your own Go module proxy (Artifactory, Athens) still has every version you ever fetched. You probably run one already.
| You want | Without a fork | With a fork |
|---|---|---|
| Your own conventions | Yes: dedo.yaml | Yes: still dedo.yaml |
| The bytes you installed never change | Yes: go.sum, the checksum database, buf.lock, SHA-256 pins | Yes, from your own repository |
| Builds that survive upstream vanishing | Yes, with your own Go proxy | Yes |
| A person you trust reviews every change before any service gets it | No | Yes |
| Fixes on your own schedule | No | Yes (send them upstream too) |
| Your own BSR module and plugin, private if you like | No | Yes |
Only the last three rows need a fork. If none of them is a hard requirement, close this tab, write a house rule, and get on with your day.
When you really do need one
Some organisations have a rule that every line of third-party code that writes code into their services, runs in their CI and briefs their coding agents gets read by one of their own people first. That’s a fair rule. dedo does all three of those things.
For them, dedo supports a fork as a first-class setup, not a hack. The whole design comes down to one sentence:
A fork changes names, never behaviour.
scripts/fork.sh renames the Go modules, the options module on the BSR, the Wasm lint plugin, the MCP Go SDK build services use, and the links in lint findings. Everything a service’s protos can see stays the same: the dedo.v1 package, every option name and extension number, every rule ID, the dedo.yaml format, and the names of the binaries. Moving a service to the fork changes no .proto file.
Two details that save an afternoon:
- Rename, don’t
replace.go install pkg@versionignoresgo.workand refuses a module whosego.modhas areplacedirective. A renamed module installs like any other. - Upstream or the fork, never both. Each one registers
dedo/v1/options.protowith the protobuf runtime, and the second registration panics at startup. Every library a service links must make the same choice.
Making the fork
Say you’re acme. First, copy the MCP Go SDK build to your organisation. It is the upstream SDK plus one fix, with nothing of anyone’s in it, and it goes away when the fix ships in an SDK release:
git clone --mirror https://github.com/raveesh-me/go-sdk.git
cd go-sdk.git && git push --mirror https://github.com/acme/go-sdk.gitCode language: Bash (bash)
Then create acme/dedo and push upstream into it. GitHub’s fork button makes a public fork, so for a private one create an empty private repository. Keep upstream’s tags out: the fork tags its own commits with upstream’s version numbers.
git clone https://github.com/raveesh-me/dedo.git && cd dedo
git remote rename origin upstream
git config remote.upstream.tagOpt --no-tags
git remote add origin https://github.com/acme/dedo.git
git push -u origin mainCode language: Bash (bash)
Rename, test, and open a pull request:
git fetch upstream '+refs/tags/*:refs/upstream-tags/*'
git switch -c fork-v0.24.0 refs/upstream-tags/v0.24.0
GO_MODULE=github.com/acme/dedo SDK_MODULE=github.com/acme/go-sdk \
BSR_OWNER=buf.build/acme BSR_VISIBILITY=private scripts/fork.sh
make test
git commit -am "chore: fork dedo v0.24.0 as acme/dedo"Code language: Bash (bash)
The script stops if any upstream name is left. It never edits generated code: a .pb.go file carries go_package inside a length-prefixed descriptor, and editing it by hand makes the package panic at init. It regenerates instead.
Add a BUF_KEY secret, protect main and the tags, turn off Dependabot (upstream already takes its updates, and you get them in the next sync), then release with upstream’s version number: the four module tags first, then v0.24.0 on its own. A version then means the same code in both places.
Need a fix before upstream ships it? Release it as a pre-release of the next patch, v0.24.1-acme.1. Semantic versioning sorts it after v0.24.0 and before upstream’s v0.24.1, which replaces it. Install it by name with dedo update -version v0.24.1-acme.1. And send the fix upstream. Please. That’s the cheapest fork maintenance there is.
Public or private
That’s your call. The difference is in how services install the fork.
A public fork needs no setup. The Go proxy caches it, the checksum database records every version, and anyone installs it with the fork’s path. It’s also tamper-evident for free: once a version has been fetched, nobody, you included, can change what it contains.
A private fork needs Go to fetch it from git, with credentials, on every machine and in every CI job:
go env -w GOPRIVATE=github.com/acme/*
gh auth setup-git
go install github.com/acme/dedo/cmd/[email protected]
dedo tools # the lint plugin comes from the fork tooCode language: Bash (bash)
In CI, give jobs a GitHub App that can read only acme/dedo and acme/go-sdk, and mint a short-lived token per job. With many repositories, your own Go proxy with GONOSUMDB=github.com/acme/* scales better than tokens everywhere. Either way, the public checksum database never sees a private module, so each service’s committed go.sum is your only record of what a release contained. Protect your tags.
The options module follows the same choice. It can live in your organisation on buf.build (what the script sets up), on your own BSR instance, or, if you really insist, as a copy of proto/dedo in each repository. That last option works, but you’ll be copying it again on every upgrade.
Moving a service to the fork
With the fork’s dedo installed (same binary name, so it replaces upstream’s in $GOBIN), it’s a rename and an upgrade:
git grep -lzE 'raveesh-me/(dedo|go-sdk)|buf\.build/r-a/dedo' -- . ':!*.pb.go' | xargs -0 perl -pi -e '
s{github\.com/raveesh-me/dedo}{github.com/acme/dedo}g;
s{github\.com/raveesh-me/go-sdk}{github.com/acme/go-sdk}g;
s{buf\.build/r-a/dedo}{buf.build/acme/dedo}g'
dedo upgrade # buf dep update, regenerate, go mod tidy, the fork's lint plugin
make check testCode language: Bash (bash)
Move all of a service’s libraries in one change. Remember the panic.
Taking upstream releases
Because the fork’s only change is names, a sync is a merge that prefers upstream’s lines, followed by the script:
git fetch upstream '+refs/tags/*:refs/upstream-tags/*'
git switch -c sync-v0.25.0 origin/main
git merge -X theirs --no-edit refs/upstream-tags/v0.25.0
GO_MODULE=github.com/acme/dedo SDK_MODULE=github.com/acme/go-sdk \
BSR_OWNER=buf.build/acme BSR_VISIBILITY=private scripts/fork.sh
make test
git diff refs/upstream-tags/v0.25.0 -- . ':!*.pb.go' # should be names onlyCode language: Bash (bash)
The final diff should show nothing but names. If it shows anything else, that’s your cue to look more closely.
Then comes the part you forked for: the review. Read upstream’s changes as code that runs with your access:
| Path | Why it matters |
|---|---|
cmd/dedo | Runs go, buf and make on developers’ machines and in CI |
lint | Runs in every service’s CI and editor |
mcp, openapi, templates | Write code and config into every service |
guidance | Goes into every service’s AGENTS.md and CLAUDE.md. Read it as instructions to your coding agents, because that’s exactly what it is |
.github/workflows | Runs in the fork, with its secrets |
go.mod, go.sum | New or bumped third-party dependencies |
A scheduled workflow can watch for upstream tags and open the sync pull request. The review stays with a person. That’s the whole point.
The supply-chain checklist
- Protect
main: pull requests only, reviewed by the fork’s owners. - Protect tags: only release managers create them, and nobody moves or deletes one.
- Release only from the release job, with
BUF_KEYscoped to your BSR organisation. - Pin the fork’s GitHub Actions to commit SHAs, and again after each sync, since a merge takes upstream’s lines.
- Commit
go.sumandbuf.lockin every service, and rungovulncheckon the fork. - Keep the fork’s changes to names only, so every sync is the script plus a review.
- Remember what a fork doesn’t cover: dedo’s own dependencies still come from the Go proxy and buf.build. Mirror them too if your policy says so.
The point
dedo is built so you don’t have to own it to make it yours. Your conventions go in dedo.yaml, and the pins guarantee you get exactly what you installed. Upstream does the maintaining.
Some organisations still need every line reviewed in-house before it ships. For them, a fork is supported, scripted, and as small as it can be: the same code under your names, one merge and one script per release.
Fork if you must. Most of you mustn’t.
The full guide, with every renamed asset and the CI setup for a private fork, is forking.md.
The dedo series
- Part 1: Define Once, Deploy Everywhere: why one proto can carry the contract and its rules.
- Part 2: Getting Started with Todo Good and Todo Bad: two services, one interface, and what each lint rule protects you from.
- Part 3: Adding dedo to an Existing Service: one layer at a time, with an audit mode before you enforce.
- Part 4: House Rules, Your Conventions as Lint: your organisation’s conventions as CEL checks, shared from one repository.
- Part 5: Multi-Tenant SaaS Auth: the tenant boundary, declared in the proto and enforced on every transport.
- Part 6: How fast is Dedo?: what dedo costs and saves, measured: getting started, lint, and MCP tool calls a second.
- Part 7: Forking dedo (You Probably Don’t Need To) (this post): why house rules and pins cover most needs, and how to run a fork when policy demands one.

Leave a Reply