ConstructiCat Logo
CodeBust.
Browse section ▾

Hardcoded Configuration.

AI assistants bake environment-specific values — URLs, ports, paths, timeouts, keys, and magic numbers — directly into logic instead of reading them from config or environment, because a literal is the most probable next token and the model lacks awareness of your existing config layer.

##Signs and Symptoms

An AI assistant tends to emit a plausible literal exactly where a reference belongs. The code runs in the demo, so the smell survives review unless you look for it.

Tell-tale signs:

  • Inline endpoints, ports, paths, and timeouts sprinkled through business logic instead of a config module: http://localhost:3000, /tmp/cache, 5432, setTimeout(..., 30000).
  • Magic numbers/strings with no named constant — retry counts, page sizes, rate limits, feature thresholds.
  • The same literal repeated across files because the model regenerated it rather than importing an existing constant (the GitClear "copy/paste up, refactor down" pattern in miniature).
  • Hardcoded credentials / keys — API tokens, encryption keys, or Basic auth strings pasted from training data.
  • An existing config.ts / .env / settings object that the new code ignores entirely.
  • Environment-baked values: a dev/staging URL or test account ID hard-wired into a path that ships to prod.
// 🚩 AI-generated: every knob is a literal, dev values baked in
export async function syncOrders() {
  const res = await fetch("https://api.staging.acme.dev/v1/orders", {
    headers: { Authorization: "Bearer sk_test_4eC39HqLyjWDarjtT1zdp7dc" },
    signal: AbortSignal.timeout(30000),
  });
  const orders = (await res.json()).slice(0, 50); // why 50?
  for (let i = 0; i < 3; i++) { /* retry 3x... why 3? */ }
}

The Authorization line is a gitleaks/Semgrep hit; the URL, 30000, 50, and 3 are magic-value smells; and api.staging.acme.dev is a dev value about to reach production.

##Reasons for the Problem

Why models produce it

  • A literal is the highest-probability next token. Given fetch(, the cheapest completion that satisfies the immediate prompt is a concrete URL string, not config.apiBaseUrl. Resolving a reference requires knowing a symbol exists elsewhere; emitting a value requires nothing. Models optimize for a locally-plausible, runnable snippet.
  • Training corpus is self-contained snippets. Tutorials, README examples, and Stack Overflow answers inline their values so they run standalone. The model learned that "good example code" hardcodes — the opposite of production hygiene.
  • No repo context / limited window. The model usually hasn't read your config/, env schema, or constants file, so it can't reuse them. GitClear ties exactly this to the rise in duplication: assistants are "less likely to propose reusing a similar function… partly because of limited context size," with 5+-line duplicated blocks up ~8× in 2024 while refactored ("moved") lines fell from ~24% to ~9.5%.
  • Sycophancy / answer-the-prompt bias. Asked to "add an orders sync," the model delivers something that works now; externalizing config is extra scaffolding it won't volunteer unless told to.
  • Training-cutoff staleness. Baked-in literals are often stale literals — old endpoints, deprecated API versions, default ports, or weak crypto constants (MD5, hardcoded keys). OX Security found 62% of AI-generated code ships with issues and attributes hardcoded keys/secrets/paths to patterns "learned from legacy codebases… [with] zero awareness that security standards evolved."

Why it hurts

  • Maintainability — Shotgun Surgery. Changing one timeout or base URL means hunting every duplicated literal across the codebase; miss one and behavior silently diverges per call site.
  • Correctness across environments. A staging URL or dev account ID hardwired into logic ships to prod; magic thresholds with no name drift out of sync with the values they should mirror.
  • Security. Hardcoded tokens/keys are credential leaks — once committed they live in git history forever. The arXiv build-code study (2601.16839) found hardcoded paths/URLs and high-severity hardcoded credentials recurring in AI-generated build files.
  • Review load & tech debt. Reviewers must verify every literal's provenance. The AI-specific-smells work (arXiv 2509.20491) notes models handle "in-scope literals" fine but struggle with value-sensitive cases where correctness depends on thresholds that propagate through helpers — precisely the hardcoded values humans must now audit by hand. Per Stack Overflow's Q&A with Factory's Eno Reyes, baseline code quality is "the only signal" for whether agents accelerate or decelerate a team — and hardcoded sprawl erodes exactly that baseline.

##Treatment

Prompting / review tactics

  • Point the model at your config surface: "Read src/config.ts and .env.example; use config.* / process.env for every URL, port, timeout, and credential. Do not introduce literals." Models inline because they don't know the symbol exists — name it.
  • Forbid magic values explicitly: "No magic numbers or strings — extract named constants." Then make it run eslint --rule no-magic-numbers and a secret scanner (gitleaks/Semgrep) and fix what they flag. Per Factory's guidance, wire linters/scanners into the loop so the agent self-corrects instead of leaning on human review.
  • Provide the env schema (zod/envalid/.env.example) so the model has somewhere to put values rather than guessing them.
  • Grep new diffs for http, localhost, IP literals, Bearer , and bare digits in call args before merging.

The refactor — name the classic moves:

  • Replace Magic Number with Symbolic Constant for thresholds, counts, sizes.
  • Extract Function / Extract Config Module to collect knobs into one typed, validated place.
  • Externalize to Environment (12-factor) for anything that differs per environment or is secret; never commit secrets.
// config.ts — single, validated source of truth
import { z } from "zod";
const env = z.object({
  ORDERS_API_BASE_URL: z.string().url(),
  ORDERS_API_TOKEN: z.string().min(1),
  ORDERS_TIMEOUT_MS: z.coerce.number().default(30_000),
  ORDERS_PAGE_SIZE: z.coerce.number().default(50),
  ORDERS_MAX_RETRIES: z.coerce.number().default(3),
}).parse(process.env);
export const config = env;
// ✅ after: references, not literals — one place to change, secret out of source
import { config } from "./config";
export async function syncOrders() {
  const res = await fetch(`${config.ORDERS_API_BASE_URL}/v1/orders`, {
    headers: { Authorization: `Bearer ${config.ORDERS_API_TOKEN}` },
    signal: AbortSignal.timeout(config.ORDERS_TIMEOUT_MS),
  });
  const orders = (await res.json()).slice(0, config.ORDERS_PAGE_SIZE);
  for (let i = 0; i < config.ORDERS_MAX_RETRIES; i++) { /* ... */ }
}

If a secret already landed in a commit, rotate it — removing the line doesn't purge git history.

##Detected by