<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nikhil Kumar </title>
    <description>The latest articles on DEV Community by Nikhil Kumar  (@nickelsec).</description>
    <link>https://dev.to/nickelsec</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4112586%2F1131bbfb-6580-4222-ae95-8f479fa82b60.jpg</url>
      <title>DEV Community: Nikhil Kumar </title>
      <link>https://dev.to/nickelsec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nickelsec"/>
    <language>en</language>
    <item>
      <title>I parsed 76,801 lines of my own Claude Code history. Three things nearly broke my parser.</title>
      <dc:creator>Nikhil Kumar </dc:creator>
      <pubDate>Sun, 06 Sep 2026 16:42:15 +0000</pubDate>
      <link>https://dev.to/nickelsec/i-parsed-76801-lines-of-my-own-claude-code-history-three-things-nearly-broke-my-parser-h44</link>
      <guid>https://dev.to/nickelsec/i-parsed-76801-lines-of-my-own-claude-code-history-three-things-nearly-broke-my-parser-h44</guid>
      <description>&lt;p&gt;Claude Code writes every session to &lt;code&gt;~/.claude/projects&lt;/code&gt; as JSONL. I wanted to know what I'd actually built over three weeks on one repo, so I started reading those files.&lt;/p&gt;

&lt;p&gt;The format is undocumented. Here is what I measured across 28 transcripts, 275 MB and 76,801 lines, and the three things that gave me wrong numbers before I noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The files replay, and counting lines overcounts by 3x
&lt;/h2&gt;

&lt;p&gt;Claude Code appends to a transcript when you resume or rewind a session, and it rewrites records it has already written. The same &lt;code&gt;uuid&lt;/code&gt; shows up more than once.&lt;/p&gt;

&lt;p&gt;My largest session:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lines&lt;/td&gt;
&lt;td&gt;36,676&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;distinct records&lt;/td&gt;
&lt;td&gt;10,964&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uuids appearing more than once&lt;/td&gt;
&lt;td&gt;7,919&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So counting lines, which is the obvious first thing anyone does, overstates that&lt;br&gt;
session by more than three times. Worse, the overstatement is uneven, so there&lt;br&gt;
is no constant you can divide by to fix it.&lt;/p&gt;

&lt;p&gt;Deduplicate on &lt;code&gt;uuid&lt;/code&gt;, keeping the last copy. Later copies carry fields the&lt;br&gt;
earlier ones did not.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. One field is sometimes an object and sometimes a string
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;toolUseResult&lt;/code&gt; is an object 17,319 times and a bare string 490 times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// This is wrong, and it fails quietly&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Record&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ToolUseResult&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ToolUseResult&lt;/span&gt; &lt;span class="s"&gt;`json:"toolUseResult"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you type it as an object only, every string-valued line fails to decode. And&lt;br&gt;
if your reader skips a record on a decode error, which is a reasonable default,&lt;br&gt;
you lose it silently. In my own test fixture that dropped 85 of 340 records. I&lt;br&gt;
found it a week later, by accident, because a count didn't match.&lt;/p&gt;

&lt;p&gt;The fix is a custom unmarshaller that accepts either shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ToolUseResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;UnmarshalJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sc"&gt;'{'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;plain&lt;/span&gt; &lt;span class="n"&gt;ToolUseResult&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;message.content&lt;/code&gt; has the same problem: a list of blocks 52,641 times, a bare&lt;br&gt;
string 400 times.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Quiet commits arrive with no hash
&lt;/h2&gt;

&lt;p&gt;When the agent commits, the result carries the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"toolUseResult"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gitOperation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"commit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"d0a65cc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"committed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except Claude Code fills that in by reading what git printed. Silence git and&lt;br&gt;
the field never appears:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;commit command&lt;/th&gt;
&lt;th&gt;calls&lt;/th&gt;
&lt;th&gt;carried a hash&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;without &lt;code&gt;-q&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;td&gt;53 (93%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;with &lt;code&gt;-q&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Relying on that field alone found 4 of one repository's 31 commits.&lt;/p&gt;

&lt;p&gt;The dependable signal is the shell command itself: a &lt;code&gt;git commit&lt;/code&gt; that returned&lt;br&gt;
without an error. Which leads to the trap I enjoyed least.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bonus trap: commits that never happened
&lt;/h2&gt;

&lt;p&gt;If you detect commits by matching &lt;code&gt;git commit&lt;/code&gt; in shell commands, a command that&lt;br&gt;
&lt;em&gt;writes a file&lt;/em&gt; containing those words looks identical to one that runs them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; release.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SH&lt;/span&gt;&lt;span class="sh"&gt;'
git commit -m "release &lt;/span&gt;&lt;span class="nv"&gt;$VERSION&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;SH
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven of one project's apparent commits were heredocs like this. The fix is to&lt;br&gt;
check whether a heredoc opens before the match. A real commit taking its message&lt;br&gt;
on a heredoc opens it &lt;em&gt;after&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why any of this mattered to me
&lt;/h2&gt;

&lt;p&gt;I was building a thing that draws your Claude Code history as a diagram: squares for days, smaller squares for tasks, a circle for every prompt, with commits marked on the work that produced them. Every number above is one I got wrong first and had to go back and fix.&lt;/p&gt;

&lt;p&gt;It's a Go binary, runs locally, reads files already on your disk and sends&lt;br&gt;
nothing anywhere. &lt;a href="https://github.com/nickelsec/bough" rel="noopener noreferrer"&gt;github.com/nickelsec/bough&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full format notes, including record types and the fields that carry less&lt;br&gt;
than you'd hope, are at &lt;a href="https://www.bough.run/docs/format" rel="noopener noreferrer"&gt;bough.run/docs/format&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're parsing these files yourself, the replay is the one that will get you.&lt;br&gt;
It's silent, it's uneven, and every number downstream of it is wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>data</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
