You asked Claude Code to add a loading state to the sign-in form. The loading state works. The button also says “Log in” now instead of “Sign in”, the error message got friendlier, and one heading is suddenly sentence-cased. Nobody asked for any of that, every test passes, and unless someone reads the whole diff word by word, it ships.
This post is about that specific failure: existing, approved copy being silently reworded by an AI coding agent, and the workflow that prevents it. Strings that agents create hardcoded and un-keyed are a different problem, covered in why AI coding tools keep adding hardcoded strings. Where product copy should live in general is the architecture question, covered in code-native product copy. This one stays on the incident — the Monday morning where the wording changed and nobody decided it should. globalize.now is a code-native copy management system with localization built in, and this incident is one of the reasons it exists.
Why do AI coding agents rewrite UI copy?
AI coding agents rewrite UI copy because they see prose sitting in components; once that copy lives behind keys in a source locale file, changing it becomes an explicit, reviewable edit instead of a side effect of some other task.
Inside a component, “Sign in” has no special status. It is a string literal in a JSX attribute, indistinguishable from a className or a test id. The agent was trained on millions of diffs where improving nearby code was good behaviour, and to a model, making a label “clearer” is the same move as renaming a variable. There is no type error waiting, no failing test, no owner annotation on the words. Rewording costs the agent nothing.
The change then survives review for the same reason. Reviewers read logic. A wording change sits in the middle of a diff that is legitimately about a loading state, it compiles, it renders, and if a snapshot test complains, the routine response is to update the snapshot — which records the new wording as if it were approved.
Do rules files like AGENTS.md actually stop it?
Agent rule files reduce the frequency; they do not make UI copy unwritable. Architecture is what makes the edit visible.
Have the rules file anyway — it is the cheapest layer and it works often enough to be worth thirty seconds. This block works in AGENTS.md, CLAUDE.md, and Cursor rules (.cursorrules or .cursor/rules/):
## UI copy policy — do not modify user-facing text
- Never change user-visible strings (button labels, headings, empty states,
error messages, tooltips, placeholder text) unless the task explicitly
asks for a copy change.
- User-visible text lives in the source locale file (e.g. locales/en.json).
Reference it by key. Never inline a new user-facing string in a component.
- If a task needs new user-facing text, add a key to the source locale file,
reference it, and list the new key in your summary for copy review.
- Any diff to the source locale file is a copy change. Call it out
separately — it needs its own approval, apart from code review.Now the honest part. A rule is context, and context competes. On a long task the instruction is one line among thousands of tokens of code, and it loses sometimes — to a compaction, to a sub-agent that never saw it, to a strong pattern in the surrounding file. Worse, the agent that breaks the rule does not announce it. You get no notification that your wording changed; you get a diff in which it happens to have changed. A convention is not a check.
So the rules file is the first layer, and the layer you should not trust by itself.
How do you make a copy edit visible instead of silent?
Move every user-visible string out of components and behind keys in a source locale file. After that, an agent that wants to change your wording has to edit a file whose entire purpose is wording.
This is the step the rules file cannot do, and the mechanism is plain. When components reference t('auth.signIn') instead of restating “Sign in”, two things change at once. Component diffs stop containing copy, so the agent working on your loading state has nothing to reword where it is working. And every copy change, agent or human, lands in one file — where a changed line is the event you care about, not noise around it. Scanning a locale-file diff takes seconds. Reverting a bad reword is one line.
To be precise about what this buys: the agent can still edit the locale file. The point is not that the words become untouchable — it is that touching them stops being a side effect and becomes a legible, reviewable act. That is the difference between finding out from the diff and finding out from a customer.
The full case for keeping the source of truth in the codebase — rather than in a design tool that syncs to it — is the pillar, code-native product copy. This post only needs the one consequence: keyed copy is what turns your rules file from a request into something checkable.
What does the review gate look like?
Treat any diff to the source locale file as a copy change, and route it to whoever owns the words.
Concretely: put the locales directory in CODEOWNERSso a copy-owning human is requested on any change to it. Add one line to the PR template — “does this PR change user-facing wording, and was that the task?”. And keep the rules-file requirement that the agent lists changed keys in its summary; when it complies, review is instant, and when it does not, the CODEOWNERS hook catches it anyway.
On a small team this is lighter than it sounds. The locale diff is short, it reads in plain language, and “should this button say Log in now?” is a decision someone can make in five seconds — provided the question is actually put in front of them, which is the entire job of the gate.
What if the agent already rewrote your copy?
Restore the approved wording first, then key the strings so the next reword shows up as a diff.
Finding the damage is a git exercise: diff the affected components against the last commit you trust and read only the string literals. Restore what changed. If you cannot say with confidence which wording was the approved one — the button says “Log in”, the marketing page says “Sign in”, and no artifact records which is right — you have hit the deeper state this incident feeds. Copy drift is user-visible text diverging from its approved source across surfaces and over time, with no single source of truth to diverge from. That term, what causes it and what it does downstream is its own piece: what is copy drift.
Then key what you restored, starting with the strings the agent touched. They are demonstrably the ones in the line of fire.
Where does localization fit?
A silent reword is an annoyance in one language and a liability in ten: every translation derived from the old wording is now stale, and nothing tells you to retranslate.
This is where the agent incident compounds. Ship the reworded English and your German, Japanese and Spanish still say the old thing — the surfaces disagree across languages now, not just across screens. Catalogs falling out of step is its own failure mode with its own fixes, covered in translation files out of sync.
Keyed copy closes this loop too, because a changed source string is a visible event that its translations hang off, rather than a literal that quietly moved. That is the design behind globalize.now: copy lives in the codebase, single-sourced behind keys, and localization is the same operation rather than a bolt-on — connect a repository and every language derives from that one source. Teams shipping with AI coding tools hit this earliest, which is why we write for developers and for people building with AI tools.
The next time an agent is in your components, the question is whether a changed label shows up as a decision or as a surprise. globalize.now single-sources your user-visible strings in the repository you connect, and derives every language from that one copy.
globalize.now turns hardcoded app copy into translation-ready locale files and keeps them updated as you ship.
Try globalize.now free