Here’s a comment I read in the code I was working on.
Walk the entire collection to locate flagged clusters. Skip it altogether when the cached, dirty-tracked flag count says there are none (the usual case): nothing needs marking, so the walk is pure waste. When the count is stale (e.g. right after an edit) or non-zero, we fall through to the real lookup, so stale marks never show. Same eventually-consistent signal the “Flags” badge relies on.
One thing in this comment caught my mind: the small parenthetical that says “the usual case”. If that were true, it would be important information! However, this comment is generated by the llm model Claude, which has no sense of what counts as the usual case in this domain. But by generating that comment, it tricks every future reader (both human and robot) into believing in a property of the system that does not exist.
Since I knew, in this case, that the comment was generated by a robot and that the property it contained was hallucinated, I could fix the comment. But that’s not always so easy. Here’s another comment.
AcmeRate’s live currency conversion (in our per-line totals) are incompatible with connection pooling: on a reused connection the rate lookup runs against the previous session’s locale and returns figures in the wrong currency. So fall back to a fresh connection when the order has a foreign-currency line (and therefore conversion), and keep pooling otherwise so large domestic orders stay fast. sessionInit runs before the request config is assembled, so localeState is already populated when this is read.
This comment implies that it is important that we “keep pooling” so that “large domestic orders stay fast”. If a human wrote this comment, I would assume they had thought carefully about it, and determined that the optimisation must be kept in. But if it’s written by a robot, it sounds more like it defensibly kept something in the code that it has no business deciding about, because it doesn’t know what it’s doing.1 The optimisation was useless. Only a little domain experience is needed to know that nearly every order contains a foreign currency, and profiling shows large domestic orders are fast even without pooling.
How do detect differences in language
Before we go on, I should say that while I trained the classifier on a wide variety of human authors, and comments generated from a mix of llm models, I suspect most of the robot-generated comments came from Anthropic models. I base this on the fact that the classifier reliably fails to detect comments generated by llm models from other providers. We’ll get back to this.
The interesting bit is also not the classifier itself, but what it looks at to distinguish Claude-generated comments from human ones. I’m not versed in computational linguistics, so for this project I did some cursory reading and picked up three basic ways to decompose texts to try to extract style differences. Those three methods all cut up the source text into smaller fragments, count how often those fragments appear in text, and then see whether that frequency is higher or lower for Claude-generated texts compared to human-written ones.
Character frequency (62 % accuracy)
Into characters is the most basic way to cut up text, and this does carry useful
signal. A naïve guess, for example, would be that em dashes (—) signify a
comment was generated by Claude. That’s true! Other such typographical marks
Claude produce more of than humans are semicolons (;), unicode arrows (→),
and ellipses (…).
Character frequency analysis also tells us Claude generates more syntactically
complex comments than humans: they contain more full stops (.), commata (,),
parentheses (( and )), and line breaks. Claude also surrounds literal
strings with straight, ascii quotation marks (") more than humans do, e.g.
to reference text in user interfaces. In general, Claude uses a wider variety of
symbols than humans do, and produces text with a higher symbol density.
There are two groups of symbols that appear more often in human text:
- Colons (
:) as separators introducing the next part, as I did in the line leading up to this list. - Symbols that come from code or parts of code included in comments, e.g. dollar
signs (
$), less-than and greater-than signs (<and>), backticks (`), equals signs (=), exclamation marks (!), square brackets ([and]).
Claude is less likely to include code in comments than humans are, and also less
likely to write TODO comments.
Common word frequency (64 % frequency)
The next level up in language abstraction might be words. However, at that point we have to be careful. Due to the way language works (with documents often containing hapax legomena, i.e. words that only show up once in a corpus), there’s a risk a word-frequency based classifier learns differences in subject matter rather than style. To avoid subject matter leaking in, we’ll count the frequencies of common, non-subject words only. These are sometimes known as function words.
You know what? Let’s turn it into a fun game!
The list below contains eight groups of words. Some of the groups are more common in human-written comments, and some of the groups are more common in Claude-generated comments. For each row, mark it as either “human” or “Claude” and see how many you get right!
- your of my other over since enough they be few towards
- which how this have about down could out much should
- must everything before away among the thousand past until their
- while every both one each none within per nothing once outside
- so its whether against through back would across up first several
- therefore yet when off still inside around after from at without
- more though his can although but except such second either
- we you who I will because some there these why along our
Done? Then you can check your answers. The first and last two groups (1–2 and 7–8) contain words often used in human-written comments. The middle four groups (3–6) contain words often used in Claude-generated comments.
What we find is that Claude generates comments that contain more prepositions and directions (through, up, among, within, around, after, at, etc.), whereas humans write comments with more pronouns, qualifications, and story-weaving words (we, you, because, although, except, should, either, some, etc.)
Part-of-speech frequencies (54 %)
In the previous section, we abstracted words into grammatical categories: prepositions, pronouns, etc. We can formalise this by counting the relative frequencies of part-of-speech (pos) tags. A pos tagger replaces words with their grammatical category, which can uncover sentence structures at a higher level than what’s visible through words alone.
I have to admit I’m not very skilled with grammatical categories, so I can’t comment intelligently on this, but some markers of human text include
- personal pronouns (“we”, “us”)
- existentials (“there”, “these”, when they point to something)
- modals (“should”, “can”)
- questioning pronouns (“who”, “what”)
- symbols (abbreviations like “geo” and typographical symbols like the at sign)
- superlatives (“latest”, “most”)
In contrast, Claude-generated comments contain more
- interjections (“so”, “e.g.”, “yes”)
- 3rd person singular present verbs (“affects”, “is”, “has”)
- past participle verbs (“created”, “copied”, “associated”)
- predeterminers (“all”, “half”, “less than”)
- particles (“up”, “back”, “over”, when they modify the words that come before, as in “start over”)
This hints that maybe the apparent high number of prepositions and directions in Claude-generated comments are actually serving the function of particles, rather than prepositions and directions.
That said, this is the weakest discriminator so far, at only 54 % accuracy – barely better than chance.
Character bigrams (66 %)
Instead of looking at the frequencies of single characters, we can look at the frequencies of combinations of two characters next to each other. This isn’t at a higher level of abstraction, but it does tell us more about how Claude and humans differ in how they shape their words and how they use punctuation.
The major discovery from this lens is that Claude much more frequently ends comments with a full stop. I have advocated for humans to do so for a long time4 In part because it lets a reader know if a comment is accidentally truncated, but also because it forces better sentence construction on the author of the comment, which usually improves the quality of the comment. but they haven’t caught up, so comments ending in full stops are evidence that a robot generated the comment.
Claude also has some funny quirks around word choices that start to appear at this level of abstraction. These characters are more common in the beginnings and endings of words generated by Claude than written by humans:
- Words that begin with “re-” (such as “report”, “recover”, “relocate”).
- Words that begin with “co-” (such as “coverage”, “config”, “copies”).
- Words that begin with “st-” (such as “store”, “stub”, “start”).
- Words that end with “-er” (such as “container”, “user”, “writer”).
- Words that end with “-al” (such as “terminal”, “individual”, “global”).
- Maybe also words that end with “-k” and “-p”.
Some of this I’ve noticed myself (the affinity for “re-” words), and I’m willing to believe it could maybe be a weak signal, at best. I speculate that Claude gets rlhfd into using punchier words, and those prefixes and suffixes are punchier, maybe.
Character trigrams (68 %)
Once we get into character trigrams, we have to be careful again, because character trigrams are long enough that they start to capture subject matter differences through parts of words. To reduce that risk, I extracted a list of a few hundred of the most discriminative full words, and added a preprocessing step that removes those that seemed like they leaked subject matter before passing the comments through the trigram analysis.
Looking at the difference through the character trigram lens tells us very little new. This is where we learn that Claude is trained to use the word “load” (through the “oad” trigram), as well as words that sound like “surround”, “counter”, and “account” (through the “oun” trigram.)
The reason a classifier based on character trigrams gets such a high accuracy despite not uncovering anything new is that it captures also the results from virtually all earlier layers. A trigram classifier gets many of the same signals we discovered through character frequencies, word frequencies, and bigram frequencies. Given a small enough vocabulary with a large enough training data set, it can even detect signals in pos tag frequencies.
Part-of-speech bigrams (63 %)
If we could learn from bigrams of characters, maybe we can look also at pos tag bigrams. That’s a good idea, because this is where sentence structures start to show through – and why pos tag bigrams give much higher accuracy than plain pos tag frequencies.5 Although it should be noted that pos tagging is expensive, and still not meaningfully better than mere character frequencies.
Common pos bigrams used by humans are weighted toward personal pronouns and verbs:
- Personal pronoun + verb or modal, such as “we are”, “they should”, “we may”, “we can”, “we want”, “we deprecate”, “us have”.
- Determiner + verb, such as “this guarantees”, “this is”, “this gives”.
- Personal verb + to, such as “want to”, “have to”, “move to”.
- Existential there + verb, such as “there is”, “these are”.
- Modal + verb, such as “should be”, “can cause”, “will add”.
Claude-generated comments are more abstract through adjectives, and more complex through conjunctions. More common bigrams include
- Determiner, possessive, or noun + adjective, such as “an individual X”, “a single X”, “the only X”, “its border-right X”, “its vertical X”, “its already-persisted X”.
- Conjuction + determiner, such as “and a”, “and the”, “or both”.
Although we may recognise some of these patterns from personal experience, pos bigrams are not a very powerful model alone.
Part-of-speech trigrams (65 % accuracy)
We can take it one step further and look at consecutive triples of pos tags to capture even more sentence structures. Accuracy is not much higher than for pos bigrams, but we might recognise many of the human and robot constructions as such.
Here are some examples of human-style writing, which features a lot of verbs and personal pronouns:
- “we may want”, “we can attach”, “they should upload”
- “want to store”, “have to solve”, “want to remove”
- “there is no”, “there is a”, “these are a”
- “since we do”, “after we apply”, “so we check”
- “we are looking”, “I’m guessing”, “we are determining”
- “we check the”, “we do no”, “we have a”
- “will trigger the”, “can have the”, “should cause no”
- “sends data for”, “tells users that”, “causes errors in”
In contrast, robot-generated comments are adjective-laden:
- “only difference is”, “first tab is”, “inner query aggregates”
- “col’s border-right”, “highlight’s vertical”, “layer’s already-persisted”
- “an individual checkbox”, “a single line”, “the only difference”
- “the original uploaded”, “a functioning green”, “the correct quick”
- “this flag is”, “the layout covers”, “this version does”
- “surround option affects”, “reporting service prints”, “state parameter contains”
- “thread reference autocomplete”, “used compression ratio”, “compared year title”
Looking at these examples, it would seem like robots use more big words than humans too, but I didn’t test that.6 Not in this classifier, anyway. I then went on to train another classifier, and word length wasn’t an important predictor there. You’ll hear more about that classifier later.
All features at once (75 %)
If we jam all features at once into the model to try to get them to cancel out their redundancies, here is what remains, in order of most predictive isolated feature to least:
- Claude uses em dashes more than humans.
- Claude uses the connective interjection “so” more than humans.
- Claude ends comments with full stops more than humans.
- Claude uses semicolons more than humans.
- Claude uses parentheses more than humans.
- Claude uses the possessive form “its” more than humans.
- Claude uses adjectives more than humans.
- Claude uses the word “whether” more than humans.
After these top eight, other predictors start to get fuzzy and difficult to interpret in isolation. I suspect much of the power of the classifier does not come from individually strong signals (like the em dash and other typographic quirks), but from combinations of other signals that collectively paint a coherent picture. But those combinations might be different for different documents, and they won’t show up as individual predictors in a list like this.
It would be a cool experiment to have the classifier output the relevant feature combinations when the user hovers over parts of an input to get a better sense of how the classifier sees text, but I haven’t built that.7 I did build that for the next classifier I trained, but more on that some other day.
Steps to build a classifier
I can’t share much of the code or data for this classifier because it’s partly trained on personal details.8 E.g. one of the repositories that contain examples of human comments is the text adventure I’m developing for my children. It needs to be private because it involves a lot of actual incidents and information on our family and relatives. What I can do is speak a little in generics, and then, next week, release the code for a different classifier trained fully on open data!
Either way, the first step to building any classifier is producing labeled data. In this case, that would be a large set of examples of both robot-generated comments and human-generated comments, where each example is annotated with what category it belongs to. I didn’t have that.9 The next model is trained on that. It was expensive. 💸 Instead, I settled for a proxy: the Internet gives us access to repositories of code that didn’t get any robot-generated comments a few years ago, but whose comments added recently are mainly generated by robots. We can pick an arbitrary date – let’s say October 2025 – and train our model to classify comments as written before or after that cutoff date. This will accidentally make it classify comments as written by humans or robots, too, although the overlap won’t be perfect.
Then we can write a script that goes through the git log and creates a large file with code comments and their date of addition. The script appends special start-of-comment and end-of-comment tokens to give the classifier an opportunity to discriminate on how comments end and begin. Whitespace is preserved, so any style choices surrounding whitespace make it into the model, too.
Another script reads a comment and produces feature vectors based on the lenses we’ve already discussed (character n-grams, pos tag n-grams, frequency of common words). These are smushed together and thrown into a huge logistic regression model.10 I gather the typical choice is a support vector machine, but I like the interpretability of the log-odds that come out of the logistic regression.
The features should probably be based on relative frequencies rather than absolute counts, because we likely don’t want the length of the comment to be used as a signal (longer comments tend to be robot-generated, and also produce higher absolute counts of features). The drawback of this is that when the logistic regression model makes a prediction, all the values involved are tiny percentages, so the prediction ends up underconfident for long inputs, where we would think there’s a lot of evidence. Thus, we can add a separate step that scales up the confidence by an appropriate fraction of the square root of input length, the fraction being calibrated during training.
That’s about as specific as I can get, but it covers the broad strokes. I have some ideas on a future iteration that could be made open, which would also get two other flies pregnant11 This is a translation of a Swedish mixed metaphor. Two birds with one stone in Swedish is “two flies in one swat”, but the Swedish for “one swat” (“en smäll”) sounds similar to a slang term for “pregnant” (“på smällen”). So you can say “two flies pregnant” and people will hear something’s off but won’t be able to tell if it’s an honest mistake or a joke. When I first met my wife, she loved making that joke. She has since outgrown it; I have not.: it would allow better labeling, and more model transfer.
Out-of-sample testing
I promised brief comments on generalisation uparticle, so here we go.
One natural question would be, “Can the classifier detect non-source-code-comment texts generated by Anthropic models?” and the answer is a tentative “yes”, although I haven’t spent much time evaluating that. There’s also “Can the classifier detect text by Anthropic models instructed to use a different style?” and the answer is “I don’t know”, because I haven’t tested that at all.
The third question is “can the classifier detect source code comments written by other llm models?” and the answer is a resounding “no”. The model, as trained, is very good at detecting the Anthropic house style, which means when fed source code comments generated by other llm models, it often12 Eight times out of nine. classifies them as “not Anthropic”, which is the correct answer, but in my case is easy to misinterpret as “human-written”, which is the wrong answer.
This means there is significantly less transfer than I had at first thought! I had imagined most llm models have the same annoying style, but it seems they have detectably different annoying styles. It would be a fun project to expand the scope of the analysis to cover also other models, to see which things they have in common and which are different, but that’s going to be the adventure for next week. Sorry!