If you run pkg and nothing else, you can stop reading — nothing about this touched the binary packages, and your systems were never at risk.
If you have a git checkout of the ports tree, though, and you haven't pulled since late July, your tree has quietly diverged from upstream. Not corrupted, not compromised — diverged, in the specific way that happens when the remote rewrites history under you. Your next git pull will do something confusing rather than something useful.
Here's what happened and what to do about it.
What actually happened
| When (UTC) | What |
|---|---|
| Jul 20, 22:03 | bb14266f00bb — misc/github-copilot-cli: update 1.0.71 → 1.0.72. The commit includes the 150MB Linux Copilot CLI binary. |
| Jul 21, 05:00 | f208eb093ceb — misc/github-copilot-cli: Remove inadvertently added file. The intuitive fix. It doesn't work (see below). |
| Jul 21 | core@ freezes the ports tree. |
| Jul 22 | Freeze announced on announce@, roughly 24 hours in. Stated reasoning: the freeze exists "to limit the number of commits that will need to be re-written." |
| ~Jul 23 | main rewritten after a70c5c3fd44b. Reproduction script published. Tree thaws. |
| Jul 26 | Infrastructure repairs complete, cluster package builds resume. |
The misc/github-copilot-cli port is a Linuxulator wrapper — it exists to run the Linux Copilot binary, and deliberately does not ship that binary, because the Copilot CLI carries its own custom proprietary license. The version bump pulled the actual 150MB artifact into the commit.
That produced two distinct problems, and it's worth separating them because only one of them is about licensing.
The mechanical problem: GitHub enforces a hard 100MB per-file limit. The ports tree mirrors to GitHub. A 150MB file cannot be pushed, so the mirror simply stopped. That's how this got noticed.
The licensing problem: a proprietary binary blob now existed in the permanent, world-distributed history of a BSD-licensed repository. Every full clone of the ports tree would carry it forever.
core@ was explicit that there was no concern the tree had been compromised. This was an accident, not an intrusion.
Why deleting the file didn't fix it
This is the part worth internalizing, and the timeline shows someone reached for it first: about seven hours after the bad commit, there's a commit titled "Remove inadvertently added file."
That commit is a perfectly reasonable thing to do and it does not solve either problem.
Git stores snapshots, not diffs. A commit that deletes a file makes the file absent from the tip of the branch. The blob is still in the object database, still reachable through the earlier commit, still transferred on every clone. git log will still show it. GitHub's push check — which examines objects being pushed, not just the final tree — still rejects it.
So after the removal commit you have: a repository that still can't mirror, still contains the proprietary blob, and is now 150MB larger with two commits' worth of noise instead of one.
The only way to remove an object from git history is to rewrite the commits that reference it, which produces new hashes for those commits and every commit after them. That is unavoidable, and it is why everyone downstream has to do something.
Check whether you're affected
From inside your ports checkout:
git log --oneline -1
git log --oneline | grep -c bb14266f00bbIf the grep returns 1, your checkout contains the bad commit and has diverged from the rewritten upstream. If it returns 0, you either already fixed this or your checkout predates July 20 entirely.
The other symptom is the one most people will hit first — a git pull that refuses to fast-forward, or that proposes an enormous merge of commits you thought you already had. That's the rewrite, not a broken tree.
The fix, in order of what I'd actually reach for
Note on provenance: the timeline, commit hashes, and rebase invocation here come from the two announce@ posts linked at the end. I verified the reset script's SHA-256 against the signed announcement and read its source before describing what it does. What I have not done is replay the recovery against a live diverged checkout — this was a one-shot event on someone else's infrastructure, and I'd rather say so than imply a reproduction I didn't run. Treat what follows as sourced and read, not personally replayed. Dry-run first, and prefer the re-clone where you can.
Re-clone (recommended for almost everyone)
If your ports checkout has no local changes you care about — which describes most operator machines — delete it and clone again. It is faster than reasoning about the alternatives, it cannot go subtly wrong, and it leaves you with exactly the state upstream intends.
mv /usr/ports /usr/ports.old
git clone https://git.FreeBSD.org/ports.git /usr/portsConfirm the new tree looks right, then remove the old one. Keeping /usr/ports.old around for a day costs nothing and means an interrupted clone isn't a problem.
I'd default to this. The official script exists because re-cloning isn't always practical, not because it's the better path when it is.
The official reproduction script
For trees where re-cloning is genuinely inconvenient — a slow link, a large local diff, a poudriere host with a lot of state — core@ published a script that reproduces the rewrite locally.
fetch https://www.freebsd.org/news/2026-ports-freeze/ports-reset.sh
sha256 ports-reset.sh
# expect: 9f954e7e229a1f3f0c452a98c433d70a0dbd265fc528bca6d082068d154536e8That hash is recorded in the signed announcement, and the script's own header says so — the file is meant to be checked before it's run. I verified the published file against the announced hash while writing this and it matched. Verify it yourself anyway; that's the entire point of publishing a hash, and it costs one command.
Read the script before running it. It's a little over 100 lines of /bin/sh and it does something genuinely invasive: git fast-export from before the bad commit to HEAD, a sed filter that drops the two offending commits, then git fast-import --force. It also has a sanity check that bails out if your branch doesn't contain the commit it expects, and — the useful part — a dry-run mode:
cd /usr/ports
sh ports-reset.sh -n-n prints the commands it would run and shows you a diff of what gets dropped. Run that first. Always run that first.
Two things the script's own comments are careful about, which I'd repeat:
- Commit signatures on rewritten commits are stripped. This is deliberate — it's what makes the result byte-for-byte reproducible across everyone who runs it. Expect it; don't be alarmed by it.
- If your tree has local changes mixed in, the two hardcoded commit hashes may not match your history. The script takes them as arguments for exactly this reason, oldest first.
Afterwards it tells you to review the tree for leftover staged changes. Do that.
If you're a committer with pending work
Rebase onto the new base rather than re-cloning:
git rebase --onto f9c90a4f96f0700 0a717e9c118aa6Substituting your own branch point for the old upstream hash. And if something goes sideways during any of this, git reflog still has your pre-rewrite state — a local rewrite doesn't destroy the old objects immediately.
The part worth remembering
Two things stand out to me, and neither is "someone made a mistake." Someone will always make a mistake.
The first is that the intuitive fix made things marginally worse, and the person who reached for it was almost certainly a competent developer working fast at 5am. "Delete the file and commit" is the correct mental model for a working tree and the wrong one for history. That gap doesn't announce itself.
The second is more structural. FreeBSD's ports tree is a distributed repository, and the whole promise of a DVCS is that no single host constrains it. But the moment you mirror to a centralized forge, you inherit that forge's limits — and here a GitHub policy number, 100MB, dictated an emergency freeze and a history rewrite on a project that does not host its canonical repository there. The mirror is a convenience that turned out to have a veto.
The good news is the response was about as clean as this gets: freeze quickly to bound the damage, rewrite the minimum number of commits, publish the script so the result can be independently reproduced and verified rather than trusted, sign the announcement that carries its hash, and commit to server-side hooks so the next 150MB file gets rejected at push time instead of discovered by a broken mirror.
That last one is the actual fix. Everything else was cleanup.