Appendix B — Common pitfalls

Mistakes that cost the most time to undo

Every mistake in this appendix has cost someone at least a day of debugging. Most are easy to avoid if you know they’re coming.


1. Committing credentials to git history

The mistake: .env with an API key gets committed. You notice a week later. You add it to .gitignore and think you’re done.

The consequence: the key is still in git history forever. Anyone who clones your repo can extract it with git log -p. Bots that scan public GitHub for credentials will find it in minutes.

The fix:

  1. Rotate the credential immediately. Assume it is already leaked. Generate a new one at the provider’s dashboard. Update any places that use it.

  2. Rewrite git history with git-filter-repo:

    pip install git-filter-repo
    git filter-repo --path .env --invert-paths --force
  3. Force-push to main:

    git push origin main --force
  4. Tell every collaborator to re-clone. Their existing clones still have the credential in reflog.

Prevention: .env in .gitignore from day one. Do not commit anything you would not print on a poster.

WarningForce-pushing is dangerous

Force-push destroys history that other people may have based work on. Only do it when the alternative (a leaked credential) is worse. Rotate the credential first — if the rotation is done, the leak is a bounded problem even if the history rewrite fails.


2. PyPI name-squatting

The mistake: you develop myawesomepackage for six months, get it working perfectly, decide to publish. Go to PyPI to register the name. It’s taken.

The consequence: you either rename your package (breaking any existing users, links, and documentation) or negotiate with the squatter (usually futile). Some names are actively contested; some are cybersquatted intentionally.

The fix: on the day you name your project, go to https://pypi.org/manage/account/publishing/ and register a pending publisher with the name. This reserves it in your name against the eventual publish workflow. Costs 5 minutes. No obligation to publish anything.

Real-world example: several BuckAI-adjacent projects have discovered common single-word names taken by test packages that were never maintained. There’s no PyPI mechanism to reclaim these.

Prevention: reserve early. Reserve twice if you’re not sure of the name (register both variants, drop the one you don’t end up using).


3. Co-Authored-By: Claude/Copilot/... trailers on public commits

The mistake: you use an AI coding assistant, and it appends its own Co-Authored-By: line to your commits. GitHub reads these trailers to populate the Contributors widget on your repo’s sidebar. Now your repo shows Claude / Copilot / Gemini as a top contributor next to your students.

The consequence: the appearance on the repo homepage is that a substantial fraction of the work was done by an AI. JOSS reviewers assess “human intellectual contribution” carefully; a Contributors bar dominated by an AI raises questions you don’t want to answer.

The fix (nuclear): rewrite git history to strip the trailer:

pip install git-filter-repo
git filter-repo --partial --force --message-callback '
    lines = message.decode("utf-8").split("\n")
    lines = [l for l in lines if not l.lower().startswith("co-authored-by: claude")]
    return "\n".join(lines).encode("utf-8")
'
git push origin main --force

Note that the GitHub Contributors widget caches independently of the git graph. Even after the history rewrite, the widget can take 24-72 hours to reflect the change — and sometimes gets stuck. If it doesn’t clear itself, file a support ticket.

Prevention: turn off the trailer in your AI assistant’s settings before your first commit. For Claude Code specifically: use the /user command or check your ~/.claude/settings.local.json for the trailer setting.


4. Docker image published but visibility left as private

The mistake: your GHCR workflow succeeds, your image is published, but the default visibility on GitHub Container Registry is private. docker pull ghcr.io/yourorg/yourthing:latest fails with “unauthorized” for anyone who is not logged in as you.

The consequence: users get an install failure that looks like a broken image but is actually a permissions issue. Confusing, wastes their time and yours.

The fix: on your repo landing page, in the right-hand sidebar, find Packages → click the package → Package settingsChange visibility → Public → confirm.

Prevention: the first time your workflow succeeds, immediately go check the visibility. Consider adding a note in your release checklist.


5. Trailing setuptools_scm version like 0.0.dev0+d20260101

The mistake: you publish to PyPI, and the version string is 0.0.dev0+d20260101 instead of 0.1.0.

The consequence: users can install but import yourthing; yourthing.__version__ returns nonsense. PyPI puts your release in a low sort order. Reproducibility citations point at a fake version.

Root cause: your build workflow forgot fetch-depth: 0 on the checkout step. Setuptools_scm needs full git history to see the version tag.

The fix:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0     # <-- this line

Prevention: always fetch-depth: 0 in the workflow that builds distributions. Yank the bad PyPI release and republish.


6. Unicode glyphs missing from the JOSS template font

The mistake: you write F1 ≈ 0.95 in paper.md. The Inara/pandoc pipeline compiles your PDF. The glyph renders as a Unicode replacement character () because the JOSS template’s default font doesn’t have it.

The consequence: your PDF looks amateurish. Reviewers ask what happened. You wonder why your local Markdown preview looked fine.

The fix: replace bare Unicode maths glyphs with inline LaTeX:

F1 $\approx 0.95$          # good
F1 ≈ 0.95                  # renders as F1 � 0.95 in some fonts

Same applies to ±, , , arrows, Greek letters, and anything else outside basic Latin.

Prevention: render your paper.md to PDF early — the draft-pdf GitHub Action makes this a per-commit check. Once you’ve seen the JOSS-rendered version once, you’ll know what to watch for.


7. Notebook code that only works in Colab

The mistake: you develop your demo notebook in Google Colab. It uses !apt-get install ... in a bash cell and reads sample data from !wget https://.... You commit the notebook to your repo. A reader tries to run it locally in Jupyter.

The consequence: !apt-get fails on macOS. !wget fails on Windows. The notebook is silently Colab-only.

The fix: notebooks that ship with the package should be Colab-first but Colab-compatible-not-Colab-exclusive. Use Python-side alternatives:

import urllib.request
urllib.request.urlretrieve("https://...", "data.tif")   # works anywhere

# NOT: !wget https://...    (works only where wget is installed)

For install: use pip install in a first cell so both Colab and local runs get the deps. Guard OS-specific paths with sys.platform checks.

Prevention: test your notebooks in both a fresh Colab runtime and a local JupyterLab. If either fails, fix it before shipping.


8. Massive files accidentally in the repo

The mistake: you commit data/big_training_set.npz (2 GB), realise the mistake, and git rm it in a follow-up commit. But the file is now in history — every clone downloads 2 GB.

The consequence: cloning your repo takes forever. GitHub imposes soft limits and may warn you. PyPI won’t accept an sdist that big.

The fix: same as credentials — git-filter-repo to remove the file from history, then force-push. Same collaborators-re-clone footnote.

Prevention: Git LFS for genuine binary artefacts that must be versioned. For training data, Zenodo or Hugging Face Hub — not git.


9. paper.md word-count check that catches you by surprise

The mistake: you write a beautiful, thorough paper.md. You submit. The editor’s first pre-check note is: “Please shorten to under the 1000-word soft cap.”

The consequence: a round trip that could have been avoided by a wc -w check on your side.

The fix: the JOSS docs suggest 250-1000 words. This is a soft cap — some published papers are longer. But if you’re at 1500 and every paragraph feels essential, look at your Related-tooling section — usually the fat is there.

Prevention: count words minus code blocks minus front matter minus citation tokens:

python3 -c "
import re
text = open('paper.md').read()
text = re.sub(r'\`\`\`.*?\`\`\`', '', text, flags=re.DOTALL)
text = re.sub(r'^---.*?---', '', text, flags=re.DOTALL)
text = re.sub(r'\[@[^\]]+\]', '', text)
print(len(text.split()), 'words')
"

Aim for 800-1000. Under 500 is too short.


10. Zenodo record not showing all authors

The mistake: your first GitHub Release triggers a Zenodo archive. You open the Zenodo record and only your name appears — not your four co-authors.

The consequence: Zenodo records that don’t credit collaborators start off on the wrong foot. Fixing after the fact is possible but annoying.

Root cause: Zenodo reads CITATION.cff at release time. If your CITATION.cff is missing authors — or missing entirely — Zenodo falls back to just the GitHub-authenticated user.

The fix: edit the Zenodo record manually to add authors. Update CITATION.cff on main so future releases pick up the full list.

Prevention: get CITATION.cff right before your first tagged release.


Meta-pattern: check the observable state right after every step

The theme running through every pitfall above: check what you actually shipped, not what you think you shipped.

  • After publishing to PyPI: pip install yourthing on a different machine.
  • After publishing to GHCR: docker pull on a different machine.
  • After a release: check the Zenodo record.
  • After a git-filter-repo: check git log on a fresh clone.
  • After writing paper.md: build the PDF.

Every one of these is a 30-second check. Every one has saved someone a week of debugging.