Failure Modes/Chapter 21 of 35

Hardcoded paths & magic constants

4 min readEdit on GitHub

The most common AI-written bug by volume. Easy to write. Easy to miss in review. Painful to find later.

What it looks like

# in src/train.py — landed via PR last Tuesday
DATA_PATH = "/home/alex/data/clean_v2.parquet"
MODEL_DIR = "/tmp/models"
EMBED_DIM = 768 # why 768? nobody knows
LR = 0.0003 # ditto

This works on Alex's machine. Nowhere else. The agent will write it because "in this script I use these values" is a reasonable inference from your existing code — if your existing code already has these patterns.

Why it happens

  • The agent learns from your repo. If three existing files have hardcoded paths, the fourth will too.
  • The agent finishes a task and reports success. The script ran on Alex's machine; tests passed; PR is green.
  • The reviewer (you) sees a working PR and merges it. The path bites three weeks later when the model retrains in CI.

The agent is not malicious; it is literal-minded. It does what it sees.

The cure: configuration as data

Two layers:

1. Paths come from a config file

# config/data.yaml
train_manifest: data/manifests/train_v3.json
val_manifest: data/manifests/val_v3.json
test_manifest: data/manifests/test_v3.json
output_root: runs/

Code reads config/data.yaml. Never hardcodes a path. The config can be overridden per environment.

2. Hyperparameters come from typed config objects

@dataclass
class TrainConfig:
    seed: int
    lr: float
    batch_size: int
    n_epochs: int
    embed_dim: int
    # every knob, named, typed

No magic constants in code. If a number matters, it has a name and lives in the config.

Magic constants — the subtle case

Sometimes a constant is genuinely a property of the problem, not a hyperparameter:

SAMPLE_RATE_HZ = 16000 # required by the model
RGB_NORMALIZATION = (0.485, 0.456, 0.406) # ImageNet stats

These are fine to hardcode. But:

  • Name them clearly.
  • Comment why if it would surprise a reader (# matches whisper-large-v3 input requirement).
  • Define them once at module scope, not inline.

The line: if changing this number would change the output, it is a hyperparameter and belongs in config. If it cannot change without breaking the model contract, it is a constant and can stay in code.

Detection

Add a test that scans for path-like strings outside config:

import re
from pathlib import Path

ABS_PATH_RE = re.compile(r'["\']/[a-zA-Z][^"\']*["\']')
ALLOWED_PREFIXES = ("/dev/", "/tmp/") # exceptions

def test_no_hardcoded_paths():
    src = Path("src")
    bad = []
    for py in src.rglob("*.py"):
        for m in ABS_PATH_RE.finditer(py.read_text()):
            s = m.group(0).strip("'\"")
            if not any(s.startswith(p) for p in ALLOWED_PREFIXES):
                bad.append(f"{py}: {s}")
    assert not bad, "hardcoded absolute paths:\n" + "\n".join(bad)

Crude, but catches the common case. Add to CI.

CLAUDE.md rules that prevent this

## Paths and constants (hard rules)

- No absolute paths in src/. All paths come from config or CLI args.
- Hyperparameters live in typed config objects (Pydantic / dataclass), not as inline literals.
- Numeric constants in code require a comment explaining why they cannot change.
- The scripts/ directory is the only place where ad-hoc paths are tolerated; even then, use $HOME or pathlib.

When the agent proposes code that violates these, your /review slash command flags it.

The compounding cost

A hardcoded path costs nothing to write and a lot to live with:

  • New contributor cannot run training (path doesn't exist on their machine).
  • CI breaks when the build host changes.
  • Reproducing an old experiment means reverse-engineering whose laptop the path matched.
  • Refactors are scary because nobody is sure what depends on what.

Two minutes of discipline at write-time saves hours per quarter at maintenance time.