AI Coding Agent Hardcoding Values? How to Stop It
It passed the test by writing the answer into the function. Technically correct, and the reason your staging URL is now in three files.
An AI coding agent hardcoding values is not being lazy. It is doing exactly what you graded it on. You asked for a function that returns the right answer for a given input, the agent found the shortest path to the right answer, and the shortest path was writing the answer directly into the code. The test went green. The agent reported success. It was, on the terms it was given, correct.
The fix is not a sterner prompt. It is changing what the shortest path looks like.
What this looks like in practice
Four flavours, roughly in order of how much damage they do.
Magic numbers. A retry limit of 3, a timeout of 30, a page size of 50, each written inline at the point of use, each appearing in four different files with three different values.
Environment-specific strings. https://staging-api.internal in the client, a bucket name in the upload handler, a Stripe test key pattern in a comment. These survive right up until someone deploys.
Test-shaped code. The worst one. The test asserts that calculateDiscount(100, "GOLD") returns 85, and the implementation contains a branch that returns 85 when the tier is "GOLD". The discount logic does not exist. The test passes.
Frozen assumptions. A date, a list of country codes, a tax rate, a feature list. Correct today, silently wrong in six months, and nowhere near the config file where someone would think to look.
The first two are annoying. The third is a defect wearing a green tick. Our note on an agent changing tests to make them pass covers the neighbouring failure where the agent edits the test instead of the code, and the same incentive drives both.
Why an AI coding agent keeps hardcoding values
Three things stack up.
The agent is optimising against a checkable signal. Tests passing, the build going green, the task marked done. Generality is not in that signal. A hardcoded constant and a properly parameterised implementation produce identical evidence at the moment of grading, and one takes less work.
It cannot see your config layer unless you show it. If the agent has your settings.py in context it will often use it. If it does not, inventing a local constant is the only move available. Most of these failures are context failures wearing an intent costume.
Training data is full of examples. Tutorials hardcode. Stack Overflow answers hardcode, deliberately, to keep the example short. A model that has read millions of illustrative snippets has seen the pattern rewarded constantly, because in a tutorial it genuinely is the right call.
Four fixes that work
1. Put the config surface in the prompt
The single highest-return change. Instead of describing your conventions in prose, show the file.
Config lives in src/config.ts and is read via getConfig().
Current keys: API_BASE_URL, RETRY_LIMIT, PAGE_SIZE, REQUEST_TIMEOUT_MS.
Any value that could differ between local, staging and production
goes in that file and is read through getConfig(). If you need a new
key, add it to src/config.ts with a default and use it from there.
Do not inline URLs, timeouts, limits or keys at the call site.Two things make this work where a general instruction fails. It names the file, so the agent has somewhere to put things. And it gives an explicit procedure for the case it will actually hit, which is needing a value that does not exist yet. Without that, an agent facing a missing key defaults to inlining, because the alternative looks like it needs permission.
This belongs in your AGENTS.md file rather than in each task prompt. Written once, it applies to every session.
2. Ask for the general case in the test
If the agent is writing to the test, write tests that a hardcoded answer cannot satisfy. One assertion invites a lookup table. Three assertions across different inputs force the actual logic.
# Invites hardcoding
def test_discount():
assert calculate_discount(100, "GOLD") == 85
# Requires the real rule
@pytest.mark.parametrize("total,tier,expected", [
(100, "GOLD", 85),
(250, "GOLD", 212.50),
(100, "SILVER", 92),
(0, "GOLD", 0),
])
def test_discount(total, tier, expected):
assert calculate_discount(total, tier) == expectedThe parameterised version is not just better test coverage. It is a different problem specification. The cheapest way to satisfy it is to implement the rule, which is what you wanted in the first place.
3. Add a grep guard to CI
Prompts drift and reviews get skimmed. A mechanical check does not. This one catches the common cases and runs in under a second:
#!/usr/bin/env bash
# scripts/no-hardcoded-config.sh
set -euo pipefail
PATTERN='https?://(localhost|127\.0\.0\.1|staging|dev)[^"'"'"'`[:space:]]*'
PATTERN+='|(sk|pk)_(test|live)_[A-Za-z0-9]{8,}'
PATTERN+='|AKIA[0-9A-Z]{16}'
if rg --no-heading -n -E "$PATTERN" \
--glob '!**/*.test.*' \
--glob '!**/*.spec.*' \
--glob '!**/config/**' \
--glob '!**/*.md' \
src/; then
echo
echo "Hardcoded environment value found outside config/. Move it to src/config.ts."
exit 1
fi
echo "No hardcoded environment values found."Point it at whatever your project actually uses. The important part is that it excludes tests and the config directory, so it flags the values that escaped rather than the ones that live where they should. Wire it into the same CI job as your linter and the agent gets the failure in its own feedback loop, which means it fixes the problem itself on the next iteration.
Magic numbers are harder to catch mechanically without drowning in false positives. Most linters have a rule for it. ESLint's no-magic-numbers is the usual choice, and setting it to warn with a small ignore list for 0, 1 and -1 gets the signal without the noise.
4. Review the diff for the shape, not the logic
When you read an agent's diff, the fast scan is not "is this correct". It is "does this generalise". Three questions catch almost everything:
Is there a literal here that a future reader would have to guess the meaning of?
If this ran in production instead of locally, what breaks?
Does the implementation reference the specific values from the test?
That third one takes five seconds and catches the worst category. If the numbers in the implementation match the numbers in the test, you are looking at a lookup table, not a function.
The case for leaving some values alone
Not every literal is a problem, and an agent that extracts every number into config produces its own mess. A 0 used as an array start index, a 2 in a midpoint calculation, an HTTP 200: these are not configuration, they are arithmetic and protocol. Hoisting them into a settings file makes the code harder to read and gives you a config surface nobody will ever change.
The test is whether the value could plausibly be different. Different environment, different customer, different regulatory regime, different month. If yes, it is config. If no, it is a number and it can stay where it is. Making that distinction explicit in your agent instructions prevents an over-correction that is genuinely worse than the original problem, in the same family as an agent over-engineering a solution.
Putting it together
The pattern across all four fixes is the same. Stop relying on the agent's judgement and change the environment so that the correct thing is also the easy thing. Show it the config file so it has somewhere to put values. Write tests a shortcut cannot pass. Add a check that fails loudly and automatically. Review for shape rather than re-reading logic you already asked for.
Do those and the behaviour mostly disappears, because you have removed the reason for it. What is left is a review habit worth keeping regardless, which reviewing AI-generated code before you ship it goes into properly. It is one item in a wider set of working habits collected in our overview of AI coding tools.
FAQ
Why does my AI coding agent hardcode values even when I tell it not to?
Because a negative instruction does not tell it what to do instead. "Do not hardcode the API URL" leaves the agent needing a URL with no sanctioned place to get one. Name the config file and the accessor function and the behaviour changes immediately.
Should I let the agent create new config keys on its own?
Yes, with a stated procedure. An agent that cannot add a key will inline the value instead. Give it the pattern to follow, including a default, and review the additions in the diff like any other change.
Is this worse with some agents than others?
It varies with how much context the agent gathers before writing. Agents that read surrounding files first hardcode less than agents that jump straight to editing. Either way, putting the convention in a file the agent always reads levels the difference.
How do I find hardcoded values already in the codebase?
Start with the grep guard above run over the whole repository rather than the diff. Expect a long first list. Fix the environment-specific strings first, since those are the ones that cause outages, and let the magic numbers wait for the files you were touching anyway.
Does this apply to secrets too?
Yes, and more urgently. A hardcoded secret is a hardcoded value with a much shorter fuse. Keep them out of the repository entirely, as environment variables and secrets in an AI-built app sets out, and add a scanner that fails the build rather than trusting review to catch it.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


