← Blog

Five things that broke during the hackathon (and how Opus 4.7 fixed them)

An honest postmortem of the gnarliest bugs we hit in five days — paramiko 3.x, PostGIS auto-detect, Pydantic protected namespaces, Next.js SSR, and a Git Bash path-mangling surprise.

İlhan Kılıç· 26 Apr 2026

Five days, one developer, brand-new model, brand-new platform. Of course things broke. Here are the five most-painful bugs from the build week, what they look like in the wild, and how the fix landed.

1. sshtunnel 0.4.0 + paramiko ≥ 3.5 blew up on first SSH

The agroflow nursery import script needed an SSH tunnel to the legacy MySQL server. pip install sshtunnel pulled paramiko 3.5, which removed DSSKey. The first call:

TypeError: PKey.__init__() missing 1 required keyword-only argument: 'msg'

Fix: bypass sshtunnel's auto-key-loading — load the key manually with Ed25519Key.from_private_key_file(...) and pass ssh_pkey=key_obj (object, not file path) to the tunnel constructor. Took 20 minutes of reading paramiko changelogs to land on this; Opus 4.7 got us there on the second prompt because it knew about the 3.x API change.

2. PostGIS image auto-installs the tiger geocoder, Alembic wants to drop it all

Alembic autogenerate compared the SQLAlchemy metadata against the live database and proposed dropping ~40 tables it did not recognise. They were all tiger.* — the US TIGER geocoder schema that the postgis/postgis:16-3.4 image installs by default.

Fix: in alembic/env.py, add a filter:

def _filter_object(obj, name, type_, reflected, compare_to):
    return not (reflected and name not in target_metadata.tables)

Anything reflected from the DB that isn't in our metadata gets ignored. One commit, problem disappears across all future migrations.

3. The Uzbek shapefile DBF said UTF-8, pyshp insisted on latin-1

The cadastral shapefile shipped with a .cpg file declaring UTF-8. shapefile.Reader(path) ignored it, defaulted to latin-1, and we got mojibake on every Uzbek place name (Farg'ona appeared as FargΓÇÖona).

Fix: pyshp accepts an explicit encoding kwarg. shapefile.Reader(path, encoding="utf-8") and the names render cleanly. The default-fallback behaviour ought to read .cpg but does not.

4. Pydantic v2 hates fields starting with model_

We had model_opus and model_sonnet in the settings class. Pydantic v2 raised a UserWarning: conflict with protected namespace "model_" — and worse, the validator silently dropped the values.

Fix:

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        protected_namespaces=(),  # turn off the namespace check
    )

This is a known v2 footgun. The error is loud but not loud enough — the silent drop is what bit us, and we noticed three hours later when the cascade kept routing every request to a missing model.

5. Next.js + react-leaflet SSR: ReferenceError: window is not defined

The map component imports leaflet at module load. Leaflet touches window immediately. Next.js tries to SSR the page, fails, the deploy 500s.

Fix: defer the import until the browser is ready:

const Map = dynamic(() => import("@/components/MapView"), { ssr: false });

Two extra lines, one less crash. We later wrapped this in a thin <ClientOnly> abstraction for the other browser-only components (the 3D globe hero, the leaflet-draw toolbar, the Web Speech voice input).

Bonus: Git Bash MSYS will helpfully rewrite your container paths

Running docker compose exec backend ls /data/ from Git Bash on Windows? MSYS will silently rewrite /data/ to C:/Program Files/Git/data/ before the command leaves your shell. The container then errors with "no such file" and you spend 40 minutes blaming the bind mount.

Fix: prefix the command with MSYS_NO_PATHCONV=1 or use winpty. We added a Makefile target that always sets the env var so we cannot forget.

Where Opus 4.7 actually saved us

Three of these bugs would have been a half-day each on Stack Overflow. Opus 4.7 got us to the right diagnosis on the first prompt for paramiko, and the second prompt for the Pydantic protected namespace — both because the model knew about the breaking change in the underlying library. That is the kind of compounding leverage you do not see on a benchmark, but you feel it across a 5-day sprint.

If you hit any of these on your own deploy, the fixes are committed in the repo at the line numbers in CLAUDE.md → "Sprint 1 Gotchas".