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. DevClass's summary of GitClear's research ties this to the rise in duplication: assistants are less likely to propose reusing a similar function, partly because of limited context size. GitClear's own data show blocks with five or more duplicated lines growing roughly eightfold during 2024, while refactoring-associated ("moved") lines fell from 25% of changed lines in 2021 to under 10% in 2024.
  • 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. OX Security's post on vibe-coding security traces outdated practices such as MD5 hashing to "patterns learned from legacy codebases where such practices were once industry standard" and notes that "the model has 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. An empirical study of AI-generated build code (arXiv 2601.16839) found 364 build-code smells in agent-authored pull requests, including 25 hardcoded paths/URLs and 5 hardcoded credentials rated high severity; more than 61% of those agentic PRs were merged.
  • Review load & tech debt. Reviewers must verify every literal's provenance, and values that drive behavior through helpers and thresholds are exactly the ones that are hard to audit by hand. In Stack Overflow's Q&A with Factory's Eno Reyes, Reyes observes that "the higher-quality of the code base, the more the AI accelerated the organization" โ€” 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 โ€” the catalog moves, plus one configuration practice:

  • Replace Magic Number with Symbolic Constant for thresholds, counts, sizes.
  • Extract Class to collect the knobs into one typed, validated config module.
  • Move per-environment values and secrets into the environment (12-factor config) and read them only through that module; 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