The Drift Series Part Three: Who Wrote the Drift?
Eleven examples of failures that shipped anyway and who wrote it: human, an AI agent, or both.
Jonathan Gordon
•

Part 1 and Part 2 covered what drift looks like. This article starts attaching a name to who wrote the line: a person, an AI agent, or both working together as groundwork for the question in Part 4: Does it matter?
Key findings
ReWeaver AI’s DriftDetector scanned 20 open-source repositories as part of a 125-finding manual code review. This post is the point in that review where authorship gets attached to the line: each finding below is tagged with whether the code was written by an AI agent, by a person working with an AI co-author, or where the source recorded no AI involvement at all.
Eleven findings appear here, spanning reliability, security, accessibility, testability, and architecture. Everything listed is a type of failure (something that broke, hid, or misled) in code that reported success. Where the source records an author, we’ve noted it.
Authorship was gathered from the repository’s own commit metadata or co-author trailers, where the source project recorded it. Findings without a recorded AI co-author or agent trailer are marked as "attribution not recorded." We did not assumed they were human-written.
Finding | Repo Type | Domain | Author |
|---|---|---|---|
| Bitcoin wallet for iOS and Android | Reliability | AI + Human |
| Browser city-building game | Reliability | AI |
| Open-source localization tooling | Reliability | AI |
| Browser city-building game | Reliability | AI |
| Copilot Chat extension for VS Code (Microsoft) | Reliability | AI + Human |
| AI-first visual design tool | Security | AI + Human |
| Cross-platform API client | Accessibility | Attribution not recorded |
| Browser-extension crypto wallet | Testability | AI + Human |
| Terminal AI coding agent | Architecture | AI |
| AI-first visual design tool | Maintainability | AI |
| Web scraping and crawling API | Reliability | AI |
Eleven failures that passed review
1. A wallet that silently stops telling you money arrived
REPO: Bitcoin wallet for iOS and Android (still live)
DOMAIN: Reliability
AUTHOR: AI + Human
DETECTORS: catch-warn-no-throw, error-masking-catchall
SEVERITY: Critical
THE FINDING, IN ONE LINE: A Bitcoin wallet’s push-notification registration can fail after an account import, and a comment in the shipped code — “Consider if user should be notified of notification setup failure” — records that someone saw the gap and left it open.
489 await w.fetchBalance();
490 try {
491 await majorTomToGroundControl(w.getAllExternalAddresses(), [], []);
492 } catch (error) {
493 console.warn('Failed to setup notifications:', error);
494 // Consider if user should be notified of notification setup failure
495 }
WHAT HAPPENS: The wallet imports the account and registers it for push notifications — but the registration fails. The import still reports success. The comment — Consider if user should be notified of notification setup failure — is in the shipped code. Someone saw the question and left it there.
WHAT IT COSTS: The user will never be told about money arriving at that address. Nothing will ever suggest re-importing. The only record is a console line, in a wallet whose users have no reason to open devtools.
2. A saved game that is not saved, but is reported as saved
REPO: Browser city-building game
DOMAIN: Reliability
AUTHOR: AI
DETECTORS: catch-warn-no-throw, error-masking-catchall
SEVERITY: Warning
THE FINDING, IN ONE LINE: A city-building game’s save function wraps browser-storage writes in two nested catch blocks that discard failures, so a failed save reports success and the lost progress only surfaces later as an empty slot.
51 export function writeSavedParksIndex(parks: SavedParkMeta[]): void {
52 if (typeof window === 'undefined') return;
53 try {
54 localStorage.setItem(COASTER_SAVED_PARKS_INDEX_KEY, JSON.stringify(parks));
55 } catch {
56 // Ignore storage failures (quota, privacy mode, etc.)
57 }
58 }
59
60 export function saveParkToIndex(state: GameState, roomCode?: string, savedAt: number = Date.now()): void {
61 if (typeof window === 'undefined') return;
62 try {
63 const meta = buildSavedParkMeta(state, savedAt, roomCode);
64 const updated = upsertSavedParkMeta(meta, readSavedParksIndex());
65 writeSavedParksIndex(updated);
66 } catch (e) {
67 console.error('Failed to save park to index:', e);
68 }
69 }
Saving is the one operation a user expects to be told about when it fails. Browser storage fails routinely — quota, private mode, a full disk. This is a game saving your progress.
WHAT HAPPENS: A player saves their park. writeSavedParksIndex is the only thing that actually writes, and it returns void whether it wrote or not — a quota error is caught and discarded. saveParkToIndex then wraps the whole thing in a second catch that logs to the console and returns normally. Neither function can report failure, and the caller has nothing to check.
WHAT IT COSTS: The park is gone and the game says nothing. No thrown error, no return value, no toast — only a console line the player will never open. The failure surfaces later, as an empty slot where their park used to be.
3. A service outage that tells you you’re logged out
REPO: Open-source localization tooling
DOMAIN: Reliability
AUTHOR: AI
DETECTORS: error-masking-catchall
THE FINDING, IN ONE LINE: An auth check collapses “the service is unreachable” and “you’re not signed in” into the same null return value, so a user with valid credentials is told they’re logged out during an outage.
70 try {
71 const authStatus = await ctx.localizer?.checkAuth();
72 if (!authStatus?.username || !authStatus?.userId) return null;
73 return {
74 email: authStatus.username,
75 id: authStatus.userId,
76 };
77 } catch {
78 return null;
79 }
A returned value can only mean one thing at a time. When failure and a legitimate answer share a value, the caller can’t tell them apart. This is an authentication check in a developer tool.
WHAT HAPPENS: checkAuth() is a network call. Every way it can fail — the service down, DNS, a proxy, an expired token, a 500 — is caught and converted into null, which is the same value the function returns to mean this user is not signed in. One return value now carries two opposite meanings.
WHAT IT COSTS: A user with valid credentials is told they’re not authenticated whenever the auth service is unreachable. The remedy the tool implies — sign in again — is the one thing that can’t work while the service is down, and it won’t produce a diagnosable error either. Support sees “it says I’m logged out”; the logs show nothing, because nothing was logged.
4. A button that does nothing, twice over
REPO: Browser city-building game
DOMAIN: Reliability
AUTHOR: AI
DETECTORS: catch-warn-no-throw, error-masking-catchall
SEVERITY: Warning
THE FINDING, IN ONE LINE: A “load example” button wraps every step in a single try/catch and only signals success on the final line, so any failure earlier in the sequence leaves the click producing no visible change at all.
405 onClick={async () => {
406 try {
407 const response = await fetch('/example-states-coaster/example_state.json');
408 const exampleState = await response.json();
409 saveCoasterStateToStorage(COASTER_AUTOSAVE_KEY, exampleState);
410 refreshSavedParks();
411 setStartFresh(false);
412 setLoadParkId(null);
413 setShowGame(true);
414 } catch (e) {
415 console.error('Failed to load example state:', e);
416 }
417 }}
A button either does its job or tells you it couldn’t. There’s no acceptable third option — and yet there’s a third option that’s common. This is a “load the example” button.
WHAT HAPPENS: Every step that could fail is inside the try, and the only step that makes the button appear to have worked — setShowGame(true) — is the last one. Any failure before it means the click produces no state change at all.
WHAT IT COSTS: The user clicks a button and the page doesn’t change. No spinner resolves, no error appears, nothing is disabled. The natural response is to click again, which fails the same way. The only record is a console line, in a game whose players have no reason to have devtools open.
5. An empty catch around a promise it can never catch
REPO: Copilot Chat extension for VS Code (Microsoft)
DOMAIN: Reliability
AUTHOR: AI + Human
DETECTORS: empty-catch-block, error-masking-catchall
THE FINDING, IN ONE LINE: An unawaited writeFile call sits inside a synchronous try/catch that has already exited by the time the write can fail, so a lost deep-link session marker fails with no error anywhere near the code that caused it.
115 await this._extensionContext.globalState.update(PENDING_CHAT_SESSION_STORAGE_KEY, pendingSession);
116 const pendingSessionUri = vscode.Uri.joinPath(this._extensionContext.globalStorageUri, '.pendingSession');
117 try {
118 this.fileSystemService.writeFile(pendingSessionUri, Buffer.from(${id}\n${Date.now()}, 'utf-8'));
119 } catch {
120 }
A try/catch only catches what happens inside it, synchronously. An async call that isn’t awaited has already left by the time the block ends. This is in Microsoft’s Copilot Chat extension.
WHAT HAPPENS: writeFile returns PROMISE. Line 118 calls it without await, so the call returns a promise immediately and the try block exits before the write has a chance to fail. A try/catch only catches synchronous throws — a rejected promise passes straight through it.
WHAT IT COSTS: The .pendingSession marker is how a deep link survives the window reload that opens the right workspace. When the write fails, the marker is absent, the session isn’t resumed, and the user is returned to a plain editor with no indication anything was meant to happen. The rejection surfaces, if at all, as an unattributed unhandled-rejection warning somewhere else entirely.
6. Structured data injected as raw HTML, twice
REPO: AI-first visual design tool
DOMAIN: Security
AUTHOR: AI + HUMAN
DETECTORS: security-dangerously-set
THE FINDING, IN ONE LINE: A marketing page injects two JSON-LD blocks via dangerouslySetInnerHTML with unescaped sequences — safe only as long as the content stays a hardcoded constant, with nothing recording that constraint.
161 <script
162 type="application/ld+json"
163 dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
164 />
165 <script
166 type="application/ld+json"
167 dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
168 />
Some unsafe things are safe in context. The trouble is the context lives in a different file, and nothing records that it was ever checked. This is structured data on a marketing page.
WHAT HAPPENS: This is the standard way to emit JSON-LD in React, and today it’s almost certainly safe — both objects are module constants. But JSON.stringify doesn’t escape. A string containing that sequence closes the tag early, and everything after it is parsed as markup.
WHAT IT COSTS: Nothing, until someone makes the FAQ editable, or pulls a company description from a CMS, or interpolates a page title. The change that introduces the risk will look like a content change, and it won’t touch this file. That’s exactly the class of drift worth flagging: a safe use of an unsafe primitive, with nothing recording why it’s safe.
7. A table of cookies with nothing to announce it
REPO: Cross-platform API client
DOMAIN: Accessibility
DETECTORS: table-semantics
THE FINDING, IN ONE LINE: A cookie data table ships with no caption and no accessible name, so a screen reader announces a table of nothing in particular instead of what it actually holds.
62 <p>Automatic {noticeMessage} of cookies was disabled at the time this request was made</p>
63 </div>
64 )}
65
66 <table className="table--fancy table--striped table--compact"
67 <thead>
68 <tr>
69 <th>Name</th>
70 <th>Value</th>
71 </tr>
A screen reader announces a table by its accessible name. Without one, it announces that a table exists and leaves the user to work out what’s in it, cell by cell. This table holds cookies.
WHAT IT COSTS: A data table with no caption and no accessible name is announced as a table of nothing in particular. Sighted users get the answer from the heading above it; everyone else gets a grid of values with no stated subject, and has to read every cell to work out what they’re looking at.
8. Tests that were never written, marked as skipped
REPO: Browser-extension crypto wallet
DOMAIN: Testability
AUTHOR: AI + Human
DETECTORS: test-skipped-blocks
THE FINDING, IN ONE LINE: Two position-autoclose tests for a crypto wallet’s leveraged-trading feature are marked as skipped with a lint suppression rather than implemented, so the suite reports green on risk-management paths that are never exercised.
170 // eslint-disable-next-line mocha/no-skipped-tests -- not implemented
171 it.skip('Close all: position, orders', function () {
172 // Not implemented: close all positions and orders flow.
173 });
174
175 // eslint-disable-next-line mocha/no-skipped-tests -- not implemented
176 it.skip('Position autoclose (values get updated)', function () {
177 // Not implemented: assert position autoclose and that values get updated.
178 });
A green test suite is a claim: everything we check still works. It says nothing about what’s no longer being checked. These are the tests for closing a leveraged trading position.
WHAT HAPPENS: These aren’t tests that broke and were parked. They have no body — a name, a comment saying it isn’t implemented, and a lint suppression added so that mocha/no-skipped-tests, a rule whose entire job is to catch this, won’t report it.
WHAT IT COSTS: This is the perpetuals trading surface of a crypto wallet, and the two named here are closing positions and autoclose — the risk-management paths, the ones a user reaches when a position is moving against them. The suite reports green, the coverage number counts the file, and neither flow is exercised.
9. Thirty-four mutable module globals, one per metric
REPO: Terminal AI coding agent
DOMAIN: Architecture
AUTHOR: AI
DETECTORS: global-state-leak
THE FINDING, IN ONE LINE:A telemetry module in a widely used terminal AI coding agent holds thirty-four mutable module-level globals, making initialization order implicit and every new metric a three-place edit.
789 let flickerFrameCounter: Counter | undefined;
790 let exitFailCounter: Counter | undefined;
791 let planExecutionCounter: Counter | undefined;
792 let slowRenderHistogram: Histogram | undefined;
793 let hookCallCounter: Counter | undefined;
794 let hookCallLatencyHistogram: Histogram | undefined;
795 let keychainAvailabilityCounter: Counter | undefined;
796 let tokenStorageTypeCounter: Counter | undefined;
Nobody designs a module with thirty-four mutable globals. It arrives one commit at a time, each adding the one variable that was needed. This is the telemetry layer of a widely used CLI.
WHAT HAPPENS: The line above is one of thirty-four module-level let ...: X | undefined declarations in a single block. Each is a lazily-initialized singleton; together they’re the module’s state, mutable from anywhere that imports it, and reset by nothing.
WHAT IT COSTS: Initialization order becomes implicit, tests that touch telemetry leak into each other unless every counter is reset by hand, and adding a metric means editing three places: the declaration, the initializer, and the recorder. The thirty-fifth will be added the same way, because the file now teaches that pattern to whoever opens it next.
10. A mockup drawn at three pixels
REPO: AI-first visual design tool (onlook-dev/onlook)
DOMAIN: Maintainability
AUTHOR: AI
DETECTORS: duplicated-jsx-subtree
THE FINDING, IN ONE LINE: A decorative landing-page mockup is styled with 3px font sizes and duplicated JSX at values that can’t come from any design scale — low-stakes, and included because it shows what most drift actually looks like.
31 <div className="w-16 h-fit bg-black rounded-[4px] p-1.5 select-none" style={{ fontSize: '3px' }}>
32 {/* Mini calendar header */}
33
34 <Icons.ArrowLeft className="w-1 h-1 text-foreground-primary" />
35
36
37 {new Date().toLocaleString('default', { month: 'short' })}
38
39
40 {new Date().getFullYear()}
41
Not all drift is expensive. Most of what a scan returns looks like this. This is a decorative mockup on a landing page.
WHAT HAPPENS: Lines 36 and 39 are the same element with different children — the duplication the rule names. What makes the file worth showing is everything around it: fontSize: '3px', text-[3px], rounded-[1px], w-1 h-1. Every dimension is an arbitrary value chosen to make a decorative miniature look right, and none of them can come from a scale, because no scale goes to three pixels.
WHAT IT COSTS: Little — and that’s the point. This is a landing-page mockup, not a product surface, and the honest reading is that the drift here is cheap. It’s included because a dossier that only showed expensive findings would misrepresent what a scan actually returns: most of a report looks like this, and the skill being asked of a reader is telling this apart from the wallet that stops sending notifications.
11. A cost limit is thrown, then swallowed two lines later
REPO: Web scraping and crawling API
AUTHOR: AI (199 AI commits in this file’s history)
DETECTORS: empty-catch-block, error-masking-catchall
THE FINDING, IN ONE LINE: A web-scraping API throws CostLimitExceededError when a cost cap is hit, then silently catches and discards that exact error two lines later on one code path, even though the same file re-throws it everywhere else.
140 });
141 }
142
143 if (errorResponse.error === "Cost limit exceeded") {
144 throw new CostLimitExceededError();
145 }
146 ...
176 } catch (error) {
177 if (error instanceof CostLimitExceededError) {
178 throw error;
179 }
180 ...
205
206 if (json.error === "Cost limit exceeded") {
207 throw new CostLimitExceededError();
208 }
209 } catch (e) {}
210 }
An exception is a way of saying, "Stop, this cannot continue." Catching it two lines later says the opposite. This one is about spending limits.
WHAT HAPPENS: Line 207 raises CostLimitExceededError when the upstream service reports the limit was hit. Line 209 catches it and discards it — on the very next line. The same file re-throws that exact error a few lines earlier when it arrives by a different path, so the codebase plainly treats it as must-propagate everywhere else.
WHAT IT COSTS: A scrape that exceeded its cost limit continues as a generic failure instead. Downstream code branches on instanceof CostLimitExceededError, so behavior depends on the type surviving — and on this path, it doesn’t. The cost is still incurred; only the signal is lost.
Conclusion
We cannot draw conclusions from this list of errors. They are a small subset of the larger findings of this study. They are, however, illustrative of the types of drift issues that slip through testing and review and get shipped.
Part 4 will aggregate findings and surviving lines across all 20 repositories to compute defect density by author type and test whether AI-written code drifts more overall.
Frequently asked questions
What is code drift?
Code drift is the gap between what a codebase does and what its own standards, defaults, and prior decisions say it should do — accumulating silently because it doesn’t fail a build or a test.
How is “AI-authored” determined for each finding?
We determined authorship from the repository’s commit metadata or co-author trailers, where the source project recorded it. Findings without a recorded AI co-author or agent trailer are marked as attribution not recorded — not assumed to be human-written.
Is DriftDetector free to use?
Yes. ReWeaver AI’s DriftDetector scans public GitHub repositories at no cost and with no sign-up at drift.reweaver.ai.
What does DriftDetector check for beyond bugs?
Nine production-readiness dimensions: accessibility, reliability, testability, user experience, architecture, security & privacy, AI code governance, maintainability, and design consistency — everything from an empty catch block to an unlabeled data table.
——————————————-
JONATHAN GORDON is the Founder & CEO of ReWeaver AI, a platform that detects design-code drift at the point of generation in AI-assisted development. With nearly three decades of experience, he has shaped developer tools and enterprise software at Google, Apple, Microsoft, Oracle, and SAP. He holds two patents and specializes in human-centered design for complex systems, AI/ML integration, and developer tooling.