Hardcoded paths & magic constants
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.