<?xml version="1.0"?>
<rss version="2.0"
     xmlns:dc="http://purl.org/dc/elements/1.1/"
     xmlns:dcterms="http://purl.org/dc/terms/"
     xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Tyler Cipriani: blog</title>
<link>https://tylercipriani.com/blog/</link>
<atom:link href="https://tylercipriani.com/blog/index.rss" rel="self" type="application/rss+xml"/>

<description>Tyler Cipriani</description>
<generator>ikiwiki</generator>
<pubDate>Sun, 26 Apr 2026 19:29:35 +0000</pubDate>
<item>
	<title>GitHub Actions and consequences</title>

	<guid isPermaLink="false">https://tylercipriani.com/blog/2026/04/24/on-the-software-supply-chain-doom-spiral/</guid>

	<link>https://tylercipriani.com/blog/2026/04/24/on-the-software-supply-chain-doom-spiral/</link>

	<dc:creator>Tyler Cipriani</dc:creator>



	<category>computing</category>


	<pubDate>Fri, 24 Apr 2026 20:54:04 +0000</pubDate>
	<dcterms:modified>2026-04-26T19:29:35Z</dcterms:modified>


	<description>&lt;style&gt;.title {text-wrap:balance;} #content &gt; p:first-child {text-wrap:balance;}&lt;/style&gt;
&lt;p&gt;Hackers are pwning packages at an exhausting clip.&lt;/p&gt;
&lt;p&gt;In late February, a hackerbot AI&lt;a href=&quot;https://tylercipriani.com/blog/#fn1&quot; class=&quot;footnote-ref&quot;
id=&quot;fnref1&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; yoinked the release key
for a single project. Within a month, fifty-ish other projects had cred
stealers. Each infected repo swiped credentials for the next.&lt;/p&gt;
&lt;p&gt;This spate of supply-chain hacks started from a well-known GitHub
Actions trap. A trap that AI can exploit or push us into.&lt;/p&gt;
&lt;section id=&quot;github-actions-are-a-trap&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;GitHub Actions are a trap&lt;/h2&gt;
&lt;p&gt;Trivy is an open-source security scanner. But if you used Trivy in
late March, you had a bad time.&lt;/p&gt;
&lt;p&gt;On March 19th, hackers pushed a version of Trivy that tried to
smuggle secrets from anywhere it ran. Trivy cited a “misconfiguration”
in their continuous integration (CI) system, GitHub Actions.&lt;/p&gt;
&lt;p&gt;But the exploit was less a misconfiguration and more a GitHub Actions
trap.&lt;/p&gt;
&lt;figure&gt;
&lt;img
src=&quot;https://photos.tylercipriani.com/thumbs/6f/f8ccb8fabb7987a2a2f8ae17e45d4f/large.jpg&quot;
alt=&quot;Admiral Ackbar warning about the trap in GitHub Actions&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;Admiral Ackbar warning about the trap in
GitHub Actions&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Here’s a simplified version of how Trivy got pwnd&lt;a href=&quot;https://tylercipriani.com/blog/#fn2&quot;
class=&quot;footnote-ref&quot; id=&quot;fnref2&quot;
role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre
class=&quot;sourceCode yaml&quot;&gt;&lt;code class=&quot;sourceCode yaml&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# INSECURE. DO NOT USE.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-3&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  pull_request_target&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-4&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-5&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-6&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-7&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-8&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; action/checkout@deadbeefdeadbeefdeadbeefdeadbeefdeadbeef&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-9&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-10&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;          &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; refs/pull/${{ github.event.pull_request.number }}/merge&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-11&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; ./.github/actions/setup-go&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-12&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; some/go-static-analysis@c0ffeec0ffeec0ffeec0ffeec0ffeec0ffeec0ff&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At first glance, this code looks fine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No secrets referenced.&lt;/li&gt;
&lt;li&gt;Third-party actions pinned to an immutable hash.&lt;/li&gt;
&lt;li&gt;Check out a pull request. Perform some static analysis.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But this code is a verbatim antipattern from a 2021 GitHub blog post
titled “&lt;a
href=&quot;https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/&quot;&gt;preventing
pwn requests&lt;/a&gt;”:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;if the &lt;code&gt;pull_request_target&lt;/code&gt; workflow only […] runs
untrusted code but doesn’t reference any secrets, is it still
vulnerable?&lt;/p&gt;
&lt;p&gt;Yes it is&lt;/p&gt;
&lt;p&gt;– &lt;a
href=&quot;https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/&quot;&gt;GitHub
Security Lab&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The problem is &lt;code&gt;pull_request_target&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pull_request_target&lt;/code&gt; – plunks a nice, juicy
&lt;code&gt;GITHUB_TOKEN&lt;/code&gt; into the environment.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;actions/checkout&lt;/code&gt; – takes an optional parameter
&lt;code&gt;persist-credentials&lt;/code&gt;, which removes secrets if set to
&lt;code&gt;false&lt;/code&gt;. But the default for the parameter is
&lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Setting the &lt;code&gt;persist-credentials&lt;/code&gt; parameter to
&lt;code&gt;false&lt;/code&gt; has been an open issue in GitHub Actions &lt;a
href=&quot;https://github.com/actions/checkout/issues/485&quot;&gt;since
2021&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;your-home-is-a-crime-scene&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;Your &lt;code&gt;$HOME&lt;/code&gt; is a crime scene&lt;/h2&gt;
&lt;p&gt;Once hackers had Trivy’s keys, they published a new version of Trivy
to steal more keys.&lt;/p&gt;
&lt;p&gt;LiteLLM used Trivy in their CI. The same CI they used to publish code
to PyPI, the Python software registry. When LiteLLM’s CI ran the
compromised Trivy, hackers nabbed their publishing key.&lt;/p&gt;
&lt;p&gt;And on March 24th, when Callum McMahon fired up his IDE, his MacBook
froze. And that’s how he discovered &lt;a
href=&quot;https://futuresearch.ai/blog/no-prompt-injection-required/&quot;&gt;the
LiteLLM hijack&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;McMahon’s MacBook was flailing at bad code that hackers snuck into
LiteLLM. And the bad code trying to steal credentials:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.netrc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.aws/credentials&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.config/gcloud&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.config/gh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.azure&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.docker/config.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.npmrc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.git-credentials&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.kube/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Files that are typically strewn around &lt;code&gt;$HOME&lt;/code&gt;
directories, full of tokens and keys, often unencrypted.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;ai-and-the-supply-chain-doom-spiral&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;AI and the supply chain doom spiral&lt;/h2&gt;
&lt;p&gt;We’ve dealt with problems like unencrypted credentials, unpinned
dependencies, and CI footguns forever.&lt;/p&gt;
&lt;p&gt;But AI has accelerated &lt;em&gt;everything&lt;/em&gt;, including repeating
security mistakes.&lt;/p&gt;
&lt;p&gt;On the day of the Trivy compromise, I asked Claude, “how do I scan
docker registry images for security vulnerabilities?”&lt;/p&gt;
&lt;p&gt;The reply, in part:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CI/CD Integration Example (GitHub Actions with Trivy)

    - name: Scan image for vulnerabilities
      uses: aquasecurity/trivy-action@master&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Broken in two ways:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;Unpinned references – &lt;code&gt;master&lt;/code&gt; is a reference that
changes all the time. If hackers zombify the repo, I’d be the first
victim.&lt;/li&gt;
&lt;li&gt;Active vulnerability – No mention whatsoever of the &lt;a
href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-33634&quot;&gt;CVE&lt;/a&gt; posted
that day. I never asked, so Claude never checked.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Meanwhile, Vercel’s CEO has attributed his company’s recent data
breach to a hacker that was “&lt;a
href=&quot;https://nitter.net/rauchg/status/2045995362499076169&quot;&gt;accelerated
by AI&lt;/a&gt;.” And Anthropic’s latest hype tour includes &lt;a
href=&quot;https://www.theguardian.com/technology/2026/apr/10/us-summoned-bank-bosses-to-discuss-cyber-risks-posed-by-anthropic-latest-ai-model&quot;&gt;briefing
the US Federal Reserve Chair&lt;/a&gt; about vulnerabilities unearthed by
their frontier model.&lt;/p&gt;
&lt;p&gt;Bad guys with LLMs get superpowers. Good guys with LLMs fall prey to
mid-2010’s CI problems.&lt;/p&gt;
&lt;p&gt;And the same tool that can root out &lt;a
href=&quot;https://red.anthropic.com/2026/mythos-preview/#ftnt_ref4&quot;&gt;27-year-old
security problems in OpenBSD&lt;/a&gt;, will still tell you to pin your GitHub
actions to &lt;code&gt;@master&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section class=&quot;footnotes footnotes-end-of-document&quot;
role=&quot;doc-endnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn1&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;Or somone calling themselves
&lt;code&gt;hackerbot-claw&lt;/code&gt;, at any rate.&lt;a href=&quot;https://tylercipriani.com/blog/#fnref1&quot;
class=&quot;footnote-back&quot; role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id=&quot;fn2&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;My GitHub Actions example is a
simpler verison of the action removed in &lt;a
href=&quot;https://github.com/aquasecurity/trivy/pull/10259&quot;&gt;aquasecurity/trivy
#10259&lt;/a&gt;.&lt;a href=&quot;https://tylercipriani.com/blog/#fnref2&quot; class=&quot;footnote-back&quot;
role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</description>


	<comments>//tylercipriani.com/blog/2026/04/24/on-the-software-supply-chain-doom-spiral/#comments</comments>

</item>
<item>
	<title>The future of large files in Git is Git</title>

	<guid isPermaLink="false">https://tylercipriani.com/blog/2025/08/15/git-lfs/</guid>

	<link>https://tylercipriani.com/blog/2025/08/15/git-lfs/</link>

	<dc:creator>Tyler Cipriani</dc:creator>



	<category>computing</category>


	<pubDate>Fri, 15 Aug 2025 19:55:33 +0000</pubDate>
	<dcterms:modified>2025-08-15T20:05:00Z</dcterms:modified>


	<description>&lt;style&gt;.title {text-wrap:balance;} #content &gt; p:first-child {text-wrap:balance;}&lt;/style&gt;
&lt;p&gt;If Git had a nemesis, it’d be large files.&lt;/p&gt;
&lt;p&gt;Large files bloat Git’s storage, slow down &lt;code&gt;git clone&lt;/code&gt;,
and wreak havoc on Git forges.&lt;/p&gt;
&lt;p&gt;In 2015, GitHub released Git LFS—a Git extension that hacked around
problems with large files. But Git LFS added new complications and
storage costs.&lt;/p&gt;
&lt;p&gt;Meanwhile, the Git project has been quietly working on large files.
And while LFS ain’t dead yet, the latest Git release shows the path
towards a future where LFS is, finally, obsolete.&lt;/p&gt;
&lt;section
id=&quot;what-you-can-do-today-replace-git-lfs-with-git-partial-clone&quot;
class=&quot;level2&quot;&gt;
&lt;h2&gt;What you can do today: replace Git LFS with Git partial clone&lt;/h2&gt;
&lt;p&gt;Git LFS works by storing large files outside your repo.&lt;/p&gt;
&lt;p&gt;When you clone a project via LFS, you get the repo’s history and
small files, but skip large files. Instead, Git LFS downloads only the
large files you need for your working copy.&lt;/p&gt;
&lt;p&gt;In 2017, the Git project introduced &lt;strong&gt;partial clones&lt;/strong&gt;
that provide the same benefits as Git LFS:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Partial clone allows us to avoid downloading [large binary assets]
&lt;em&gt;in advance&lt;/em&gt; during clone and fetch operations and thereby reduce
download times and disk usage.&lt;/p&gt;
&lt;p&gt;– Partial Clone Design Notes, &lt;a
href=&quot;https://git-scm.com/docs/partial-clone&quot;&gt;git-scm.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Git’s partial clone and LFS both make for:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;&lt;strong&gt;Small checkouts&lt;/strong&gt; – On clone, you get the latest copy
of big files instead of &lt;strong&gt;every&lt;/strong&gt; copy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fast clones&lt;/strong&gt; – Because you avoid downloading large
files, each clone is fast.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quick setup&lt;/strong&gt; – Unlike shallow clones, you get the
entire history of the project—you can get to work right away.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;What is a partial clone?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A Git partial clone is a clone with a &lt;code&gt;--filter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, to avoid downloading files bigger than 100KB, you’d
use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone --filter=&amp;#39;blobs:size=100k&amp;#39; &amp;lt;repo&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Later, Git will lazily download any files over 100KB you need for
your checkout.&lt;/p&gt;
&lt;p&gt;By default, if I &lt;code&gt;git clone&lt;/code&gt; a repo with many revisions of
a noisome 25 MB PNG file, then cloning is slow and the checkout is
obnoxiously large:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre
class=&quot;sourceCode bash&quot;&gt;&lt;code class=&quot;sourceCode bash&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; time git clone https://github.com/thcipriani/noise-over-git&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;Cloning&lt;/span&gt; into &lt;span class=&quot;st&quot;&gt;&amp;#39;/tmp/noise-over-git&amp;#39;&lt;/span&gt;...&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;Receiving&lt;/span&gt; objects: 100% &lt;span class=&quot;er&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ex&quot;&gt;153/153&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;ex&quot;&gt;,&lt;/span&gt; 1.19 GiB&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;real&lt;/span&gt;    3m49.052s&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Almost four minutes to check out a single 25MB file!&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre
class=&quot;sourceCode bash&quot;&gt;&lt;code class=&quot;sourceCode bash&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; du &lt;span class=&quot;at&quot;&gt;--max-depth&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;0 &lt;span class=&quot;at&quot;&gt;--human-readable&lt;/span&gt; noise-over-git/.&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;1.3G&lt;/span&gt;    noise-over-git/.&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; ^ 🤬&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And 50 revisions of that single 25MB file eat 1.3GB of space.&lt;/p&gt;
&lt;p&gt;But a partial clone side-steps these problems:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre
class=&quot;sourceCode bash&quot;&gt;&lt;code class=&quot;sourceCode bash&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; git config &lt;span class=&quot;at&quot;&gt;--global&lt;/span&gt; alias.pclone &lt;span class=&quot;st&quot;&gt;&amp;#39;clone --filter=blob:limit=100k&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; time git pclone https://github.com/thcipriani/noise-over-git&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;Cloning&lt;/span&gt; into &lt;span class=&quot;st&quot;&gt;&amp;#39;/tmp/noise-over-git&amp;#39;&lt;/span&gt;...&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-5&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;Receiving&lt;/span&gt; objects: 100% &lt;span class=&quot;er&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ex&quot;&gt;1/1&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;ex&quot;&gt;,&lt;/span&gt; 24.03 MiB&lt;/span&gt;
&lt;span id=&quot;cb4-6&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-7&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;real&lt;/span&gt;    0m6.132s&lt;/span&gt;
&lt;span id=&quot;cb4-8&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; du &lt;span class=&quot;at&quot;&gt;--max-depth&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;0 &lt;span class=&quot;at&quot;&gt;--human-readable&lt;/span&gt; noise-over-git/.&lt;/span&gt;
&lt;span id=&quot;cb4-9&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;49M&lt;/span&gt;     noise-over-git/&lt;/span&gt;
&lt;span id=&quot;cb4-10&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb4-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; ^ 😻 &lt;span class=&quot;er&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ex&quot;&gt;the&lt;/span&gt; same size as a git lfs checkout&lt;span class=&quot;kw&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;My filter made cloning 97% faster (3m 49s → 6s), and it reduced my
checkout size by 96% (1.3GB → 49M)!&lt;/p&gt;
&lt;p&gt;But there are still some caveats here.&lt;/p&gt;
&lt;p&gt;If you run a command that needs data you filtered out, Git will need
to make a trip to the server to get it. So, commands like
&lt;code&gt;git diff&lt;/code&gt;, &lt;code&gt;git blame&lt;/code&gt;, and
&lt;code&gt;git checkout&lt;/code&gt; will require a trip to your Git host to
run.&lt;/p&gt;
&lt;p&gt;But, for large files, this is the same behavior as Git LFS.&lt;/p&gt;
&lt;p&gt;Plus, I can’t remember the last time I ran &lt;code&gt;git blame&lt;/code&gt; on
a PNG 🙃.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;why-go-to-the-trouble-whats-wrong-with-git-lfs&quot;
class=&quot;level2&quot;&gt;
&lt;h2&gt;Why go to the trouble? What’s wrong with Git LFS?&lt;/h2&gt;
&lt;p&gt;Git LFS foists Git’s problems with large files onto users.&lt;/p&gt;
&lt;p&gt;And the problems are significant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;🖕 High vendor lock-in&lt;/strong&gt; – When GitHub wrote Git LFS,
the other large file systems—Git Fat, Git Annex, and Git Media—were
agnostic about the server-side. But GitHub locked users to their
proprietary server implementation and charged folks to use it.&lt;a
href=&quot;https://tylercipriani.com/blog/#fn1&quot; class=&quot;footnote-ref&quot; id=&quot;fnref1&quot;
role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;💸 Costly&lt;/strong&gt; – GitHub won because it let users host
repositories for free. But Git LFS started as a paid product. Nowadays,
there’s a free tier, but you’re dependent on the whims of GitHub to set
pricing. Today, a 50GB repo on GitHub will cost $40/year for storage. In
contrast, storing 50GB on Amazon’s S3 standard storage is $13/year.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;😰 Hard to undo&lt;/strong&gt; – Once you’ve moved to Git LFS,
it’s impossible to undo the move without rewriting history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🌀 Ongoing set-up costs&lt;/strong&gt; – All your collaborators
need to install Git LFS. Without Git LFS installed, your collaborators
will get confusing, metadata-filled text files instead of the large
files they expect.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id=&quot;the-future-git-large-object-promisors&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;The future: Git large object promisors&lt;/h2&gt;
&lt;p&gt;Large files create problems for Git forges, too.&lt;/p&gt;
&lt;p&gt;GitHub and GitLab put limits on file size&lt;a href=&quot;https://tylercipriani.com/blog/#fn2&quot;
class=&quot;footnote-ref&quot; id=&quot;fnref2&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;
because big files cost more money to host. Git LFS keeps server-side
costs low by offloading large files to CDNs.&lt;/p&gt;
&lt;p&gt;But the Git project has a new solution.&lt;/p&gt;
&lt;p&gt;Earlier this year, Git merged a new feature: &lt;strong&gt;large object
promisers&lt;/strong&gt;. Large object promisors aim to provide the same
server-side benefits as LFS, minus the hassle to users.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This effort aims to especially improve things on the server side, and
especially for large blobs that are already compressed in a binary
format.&lt;/p&gt;
&lt;p&gt;This effort aims to provide an alternative to Git LFS&lt;/p&gt;
&lt;p&gt;– Large Object Promisors, &lt;a
href=&quot;https://git-scm.com/docs/large-object-promisors&quot;&gt;git-scm.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is a large object promisor?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Large object promisors are special Git remotes that only house large
files.&lt;/p&gt;
&lt;p&gt;In the bright, shiny future, large object promisors will work like
this:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;You push a large file to your Git host.&lt;/li&gt;
&lt;li&gt;In the background, your Git host offloads that large file to a large
object promisor.&lt;/li&gt;
&lt;li&gt;When you clone, the Git host tells your Git client about the
promisor.&lt;/li&gt;
&lt;li&gt;Your client will clone from the Git host, and automagically nab
large files from the promisor remote.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;But we’re still a ways off from that bright, shiny future.&lt;/p&gt;
&lt;p&gt;Git large object promisors are still a work in progress. Pieces of
large object promisors merged to Git in &lt;a
href=&quot;https://lore.kernel.org/git/xmqqfrjfilc8.fsf@gitster.g/&quot;&gt;March of
2025&lt;/a&gt;. But there’s &lt;a
href=&quot;https://gitlab.com/groups/gitlab-org/-/epics/9094&quot;&gt;more to do&lt;/a&gt;
and &lt;a href=&quot;https://gitlab.com/groups/gitlab-org/-/epics/15972&quot;&gt;open
questions&lt;/a&gt; yet to answer.&lt;/p&gt;
&lt;p&gt;And so, for today, you’re stuck with Git LFS for giant files. But
once large object promisors see broad adoption, maybe GitHub will let
you push files bigger than 100MB.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;the-future-of-large-files-in-git-is-git.&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;The future of large files in Git is Git.&lt;/h2&gt;
&lt;p&gt;The Git project is thinking hard about large files, so you don’t have
to.&lt;/p&gt;
&lt;p&gt;Today, we’re stuck with Git LFS.&lt;/p&gt;
&lt;p&gt;But soon, the only obstacle for large files in Git will be your
half-remembered, ominous hunch that it’s a bad idea to stow your MP3
library in Git.&lt;/p&gt;
&lt;hr style=&quot;margin-top: 3em;&quot; /&gt;
&lt;div style=&quot;margin: 1em auto;&quot;&gt;
&lt;p&gt;Edited by &lt;a href=&quot;https://refactoringenglish.com/&quot;&gt;Refactoring
English&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section class=&quot;footnotes footnotes-end-of-document&quot;
role=&quot;doc-endnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn1&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;Later, other Git forges made their &lt;a
href=&quot;https://about.gitlab.com/blog/towards-a-production-quality-open-source-git-lfs-server/&quot;&gt;own
LFS servers&lt;/a&gt;. Today, you can push to multiple Git forges or use an
LFS transfer agent, but all this makes set up harder for contributors.
You’re pretty much locked-in unless you put in extra effort to get
unlocked.&lt;a href=&quot;https://tylercipriani.com/blog/#fnref1&quot; class=&quot;footnote-back&quot;
role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id=&quot;fn2&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;File size limits: &lt;a
href=&quot;https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github&quot;&gt;100MB
for GitHub&lt;/a&gt;, &lt;a
href=&quot;https://docs.gitlab.com/user/gitlab_com/#account-and-limit-settings&quot;&gt;100MB
for GitLab.com&lt;/a&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#fnref2&quot; class=&quot;footnote-back&quot;
role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</description>


	<comments>//tylercipriani.com/blog/2025/08/15/git-lfs/#comments</comments>

</item>
<item>
	<title>Per-project git commit templates</title>

	<guid isPermaLink="false">https://tylercipriani.com/blog/2025/05/21/git-commits/</guid>

	<link>https://tylercipriani.com/blog/2025/05/21/git-commits/</link>

	<dc:creator>Tyler Cipriani</dc:creator>



	<category>computing</category>


	<pubDate>Wed, 21 May 2025 19:22:45 +0000</pubDate>
	<dcterms:modified>2025-06-02T23:05:20Z</dcterms:modified>


	<description>&lt;blockquote&gt;
&lt;p&gt;People should try to compare the quality of the kernel git logs with
some other projects, and cry themselves to sleep.&lt;/p&gt;
&lt;p&gt;– Linus Torvalds&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’ll never remember your project’s commit guidelines.&lt;/p&gt;
&lt;p&gt;Every project insists on something different:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a
href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot;&gt;Conventional
commits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a
href=&quot;https://zeromq.org/how-to-contribute/#write-good-commit-messages&quot;&gt;Problem/Solution
format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gitmoji.dev/&quot;&gt;Gitmoji&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The twisty maze of &lt;a
href=&quot;https://docs.kernel.org/process/submitting-patches.html#when-to-use-acked-by-cc-and-co-developed-by&quot;&gt;trailers
in the Linux Kernel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But &lt;a
href=&quot;https://git-scm.com/docs/git-config#Documentation/git-config.txt-codecommittemplatecode&quot;&gt;git
commit templates&lt;/a&gt; help. Commit templates provide a scaffold for
commit messages, offering documentation where you need it: inside the
editor where you’re writing your commit message.&lt;/p&gt;
&lt;section id=&quot;what-is-a-git-commit-template&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;What is a git commit template?&lt;/h2&gt;
&lt;p&gt;When you type &lt;code&gt;git commit&lt;/code&gt;, git pops open your text
editor&lt;a href=&quot;https://tylercipriani.com/blog/#fn1&quot; class=&quot;footnote-ref&quot; id=&quot;fnref1&quot;
role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;. Git can pre-fill your editor with a
commit template—it’s like a form you fill out.&lt;/p&gt;
&lt;p&gt;Creating a commit template is simple.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a plaintext file – mine lives at
&lt;code&gt;~/.config/git/message.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tell git to use it:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre
class=&quot;sourceCode bash&quot;&gt;&lt;code class=&quot;sourceCode bash&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;git&lt;/span&gt; config &lt;span class=&quot;at&quot;&gt;--global&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    commit.template &lt;span class=&quot;st&quot;&gt;&amp;#39;~/.config/git/message.txt&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a
href=&quot;https://gist.github.com/thcipriani/5e79a11b69b66a493249bac1439bfe02&quot;&gt;My
default template&lt;/a&gt; packs everything I know about writing a commit.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;project-specific-templates-with-includeif&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;Project-specific templates with &lt;code&gt;IncludeIf&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The real magic of commit templates is you can have different
templates for each project.&lt;/p&gt;
&lt;p&gt;Different projects can use different templates with git’s
&lt;code&gt;includeIf&lt;/code&gt; configuration setting.&lt;a href=&quot;https://tylercipriani.com/blog/#fn2&quot;
class=&quot;footnote-ref&quot; id=&quot;fnref2&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Large projects, such as &lt;a
href=&quot;https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/SubmittingPatches?id=4e8a2372f9255a1464ef488ed925455f53fbdaa1&quot;&gt;the
Linux kernel&lt;/a&gt;, &lt;a
href=&quot;https://git-scm.com/docs/SubmittingPatches#describe-changes&quot;&gt;git&lt;/a&gt;,
and &lt;a
href=&quot;https://www.mediawiki.org/wiki/Gerrit/Commit_message_guidelines&quot;&gt;MediaWiki&lt;/a&gt;,
have their own commit guidelines.&lt;/p&gt;
&lt;p&gt;For Wikimedia work, I stow git repos in
&lt;code&gt;~/Projects/Wikimedia&lt;/code&gt; and at the bottom of my global git
config (&lt;code&gt;~/.config/git/config&lt;/code&gt;) I have:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[includeIf &amp;quot;gitdir:~/Projects/Wikimedia/**&amp;quot;]
    path = ~/.config/git/config.wikimedia&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;config.wikimedia&lt;/code&gt;, I point to my Wikimedia-specific
commit template. I also override other git config settings like my
&lt;code&gt;user.email&lt;/code&gt; or &lt;code&gt;core.hooksPath&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;an-example-my-global-template&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;An example: my global template&lt;/h2&gt;
&lt;p&gt;My default commit template contains three sections:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;Subject – 50 characters or less, capitalized, no end
punctuation.&lt;/li&gt;
&lt;li&gt;Body – Wrap at 72 characters with a blank line separating it from
the subject.&lt;/li&gt;
&lt;li&gt;Trailers – Standard formats with a blank line separating them from
the body.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In each section, I added pointers for both format&lt;a href=&quot;https://tylercipriani.com/blog/#fn3&quot;
class=&quot;footnote-ref&quot; id=&quot;fnref3&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; and
content.&lt;/p&gt;
&lt;p&gt;For the header, the guidance is quick:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# 50ch. wide ----------------------------- SUBJECT
#                                                |
#     &amp;quot;If applied, this commit will...&amp;quot;          |
#                                                |
#     Change / Add / Fix                         |
#     Remove / Update / Document                 |
#                                                |
# ------- ↓ LEAVE BLANK LINE ↓ ---------- /SUBJECT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the body, I remind myself to answer basic questions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 72ch. wide ------------------------------------------------------ BODY
#                                                                      |
#     - Why should this change be made?                                |
#       - What problem are you solving?                                |
#       - Why this solution?                                           |
#     - What&amp;#39;s wrong with the current code?                            |
#     - Are there other ways to do it?                                 |
#     - How can the reviewer confirm it works?                         |
#                                                                      |&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that’s it, except for git trailers.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;the-twisty-maze-of-git-trailers&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;The twisty maze of git trailers&lt;/h2&gt;
&lt;p&gt;My template has a section for trailers used by the projects I work
on.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#     TRAILERS                                                         |
#     --------                                                         |
#     (optional) Uncomment as needed.                                  |
#     Leave a blank line before the trailers.                          |
#                                                                      |
# Bug: #xxxx
# Acked-by: Example User &amp;lt;user@example.com&amp;gt;
# Cc: Example User &amp;lt;user@example.com&amp;gt;
# Co-Authored-by: Example User &amp;lt;user@example.com&amp;gt;
# Requested-by: Example User &amp;lt;user@example.com&amp;gt;
# Reported-by: Example User &amp;lt;user@example.com&amp;gt;
# Reviewed-by: Example User &amp;lt;user@example.com&amp;gt;
# Suggested-by: Example User &amp;lt;user@example.com&amp;gt;
# Tested-by: Example User &amp;lt;user@example.com&amp;gt;
# Thanks: Example User &amp;lt;user@example.com&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These trailers serve as useful breadcrumbs of documentation. Git can
parse them using standard commands.&lt;/p&gt;
&lt;p&gt;For example, if I wanted a tab-separated list of commits and their
related tasks, I could find &lt;code&gt;Bug&lt;/code&gt; trailers using
&lt;code&gt;git log&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre
class=&quot;sourceCode bash&quot;&gt;&lt;code class=&quot;sourceCode bash&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; TAB=%x09&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; BUG_TRAILER=&lt;span class=&quot;st&quot;&gt;&amp;#39;%(trailers:key=Bug,valueonly=true,separator=%x2C )&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; SHORT_HASH=%h&lt;/span&gt;
&lt;span id=&quot;cb6-4&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; SUBJ=%s&lt;/span&gt;
&lt;span id=&quot;cb6-5&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; FORMAT=&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;va&quot;&gt;${SHORT_HASH}${TAB}${BUG_TRAILER}${TAB}${SUBJ}&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-6&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;$&lt;/span&gt; git log &lt;span class=&quot;at&quot;&gt;--topo-order&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;--no-merges&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-7&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;at&quot;&gt;--format&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;va&quot;&gt;$FORMAT&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-8&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;d2b09deb12f&lt;/span&gt;     T359762 Rewrite Kurdish &lt;span class=&quot;er&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ex&quot;&gt;ku&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ex&quot;&gt;Latin&lt;/span&gt; to Arabic converter&lt;/span&gt;
&lt;span id=&quot;cb6-9&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;28123a6a262&lt;/span&gt;     T332865 tests: Remove non-static fallback in HookRunnerTestBase&lt;/span&gt;
&lt;span id=&quot;cb6-10&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;4e919a307a4&lt;/span&gt;     T328919 tests: Remove unused argument from data provider in PageUpdaterTest&lt;/span&gt;
&lt;span id=&quot;cb6-11&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;bedd0f685f9&lt;/span&gt;             objectcache: Improve &lt;span class=&quot;kw&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;RESTBagOStuff::handleError()&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-12&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb6-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;2182a0c4490&lt;/span&gt;     T393219 tests: Remove two data provider in RestStructureTest&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;stop-remembering-commit-message-guidelines&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;Stop remembering commit message guidelines&lt;/h2&gt;
&lt;p&gt;Git commit templates free your brain from remembering what to write,
allowing you to focus on the story you need to tell.&lt;/p&gt;
&lt;p&gt;Save your brain for what it’s good at.&lt;/p&gt;
&lt;/section&gt;
&lt;section class=&quot;footnotes footnotes-end-of-document&quot;
role=&quot;doc-endnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn1&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;Starting with
&lt;code&gt;core.editor&lt;/code&gt; in your git config, &lt;code&gt;$VISUAL&lt;/code&gt; or
&lt;code&gt;$EDITOR&lt;/code&gt; in your shell, finally falling back to
&lt;code&gt;vi&lt;/code&gt;.&lt;a href=&quot;https://tylercipriani.com/blog/#fnref1&quot; class=&quot;footnote-back&quot;
role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id=&quot;fn2&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;You could also set it inside a repo’s
&lt;code&gt;.git/config&lt;/code&gt;, &lt;code&gt;includeIf&lt;/code&gt; is useful if you have
multiple repos with the same standards under one directory.&lt;a
href=&quot;https://tylercipriani.com/blog/#fnref2&quot; class=&quot;footnote-back&quot; role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id=&quot;fn3&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;All cribbed from &lt;a
href=&quot;https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html&quot;&gt;Tim
Pope&lt;/a&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#fnref3&quot; class=&quot;footnote-back&quot;
role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</description>


	<comments>//tylercipriani.com/blog/2025/05/21/git-commits/#comments</comments>

</item>
<item>
	<title>Boox Go 10.3, two months in</title>

	<guid isPermaLink="false">https://tylercipriani.com/blog/2025/03/05/boox-go-10-3-review/</guid>

	<link>https://tylercipriani.com/blog/2025/03/05/boox-go-10-3-review/</link>

	<dc:creator>Tyler Cipriani</dc:creator>



	<category>computing</category>


	<pubDate>Wed, 05 Mar 2025 03:41:36 +0000</pubDate>
	<dcterms:modified>2025-03-13T23:11:42Z</dcterms:modified>


	<description>&lt;blockquote&gt;
&lt;p&gt;[The] Linux kernel uses GPLv2, and if you distribute GPLv2 code, you
have to provide a copy of the source (and modifications) &lt;em&gt;once
someone asks for it&lt;/em&gt;. And now I’m asking nicely for you to do so
🙂&lt;/p&gt;
&lt;p&gt;– Joga, &lt;a
href=&quot;https://archive.is/HP2Qk&quot;&gt;bbs.onyx-international.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure&gt;
&lt;img
src=&quot;https://photos.tylercipriani.com/thumbs/01/0c363d3da2755ee421a393d908b697/large.jpg&quot;
alt=&quot;Boox in split screen, typewriter mode&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;Boox in split screen, typewriter
mode&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In January, I bought a Boox Go 10.3—a 10.3-inch, 300-ppi, e-ink
Android tablet.&lt;/p&gt;
&lt;p&gt;After two months, I use the Boox daily—it’s replaced my planner,
notebook, countless PDF print-offs, and the good parts of my phone.&lt;/p&gt;
&lt;p&gt;But Boox’s parent company, Onyx, is sketchy.&lt;/p&gt;
&lt;p&gt;I’m conflicted. The Boox Go is a beautiful, capable tablet that I use
every day, but I recommend avoiding as long as Onyx continues to
disregard the rights of its users.&lt;/p&gt;
&lt;section id=&quot;how-im-using-my-boox&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;How I’m using my Boox&lt;/h2&gt;
&lt;figure&gt;
&lt;img
src=&quot;https://photos.tylercipriani.com/thumbs/f3/63eee65c63696d95c13156cd9f67ab/large.jpg&quot;
alt=&quot;My e-ink floor desk&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;My e-ink floor desk&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Each morning, I plop down in front of my &lt;a
href=&quot;https://www.amazon.com/MagicHold-Rotating-Height-Adjusting-13-15-6/dp/B01MG1EWPQ?th=1&quot;&gt;MagicHold&lt;/a&gt;
laptop stand and journal on my Boox with Obsidian.&lt;/p&gt;
&lt;p&gt;I use Syncthing to back up my planner and sync my &lt;a
href=&quot;https://www.zotero.org/&quot;&gt;Zotero&lt;/a&gt; library between my Boox and
laptop.&lt;/p&gt;
&lt;p&gt;In the evening, I review my &lt;a
href=&quot;https://mirzakhani.io/products/zen-planner-2025&quot;&gt;PDF planner&lt;/a&gt;
and plot for tomorrow.&lt;/p&gt;
&lt;p&gt;I use these apps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Obsidian&lt;/strong&gt; – a markdown editor that syncs between all
my devices with no fuss for $8/mo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Syncthing&lt;/strong&gt; – I love Syncthing—it’s an encrypted,
continuous file sync-er without a centralized server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Meditation apps&lt;/strong&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#fn1&quot; class=&quot;footnote-ref&quot;
id=&quot;fnref1&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; – Guided meditation away
from the blue light glow of my phone or computer is better.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before buying the Boox, I considered a reMarkable.&lt;/p&gt;
&lt;p&gt;The &lt;a
href=&quot;https://remarkable.com/store/remarkable-paper/pro&quot;&gt;reMarkable
Paper Pro&lt;/a&gt; has a beautiful color screen with a frontlight, a nice
pen, and a “&lt;a
href=&quot;https://remarkable.com/store/remarkable-2/type-folio&quot;&gt;type
folio&lt;/a&gt;,” plus it’s &lt;a
href=&quot;https://www.calmtech.institute/products&quot;&gt;certified by the Calm
Tech Institute&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But the reMarkable is a distraction-free e-ink tablet. Meanwhile, I
need &lt;em&gt;distraction-lite&lt;/em&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;what-i-like&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;What I like&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Calm(ish) technology&lt;/strong&gt; – The Boox is an intentional
device. Browsing the internet, reading emails, and watching videos is
hard, but that’s good.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apps&lt;/strong&gt; – Google Play works out of the box. I can
install F-Droid and change my launcher without difficulty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split screen&lt;/strong&gt; – The built-in launcher has a split
screen feature. I use it to open a PDF side-by-side with a notes
doc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading&lt;/strong&gt; – The screen is a 300ppi Carta 1200, making
text crisp and clear.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id=&quot;what-i-dislike&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;What I dislike&lt;/h2&gt;
&lt;figure&gt;
&lt;img
src=&quot;https://photos.tylercipriani.com/thumbs/10/733dfc26554fb3363483cf306f2d5a/large.jpg&quot;
alt=&quot;I filmed myself typing at 240fps, each frame is 4.17ms. Boox’s typing latency is between 150ms and 275ms at the fastest refresh rate inside Obsidian.&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;I filmed myself typing at 240fps, each
frame is 4.17ms. Boox’s typing latency is between 150ms and 275ms at the
fastest refresh rate inside Obsidian.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Typing&lt;/strong&gt; – Typing latency is noticeable.
&lt;ul&gt;
&lt;li&gt;At Boox’s highest refresh rate, after hitting a key, text takes
between 150ms to 275ms to appear.&lt;/li&gt;
&lt;li&gt;I can still type, though it’s distracting at times.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
&lt;img
src=&quot;https://photos.tylercipriani.com/thumbs/67/b6fd836c59e5ecd6aa429d5fda4ab6/large.jpg&quot;
alt=&quot;The horror of the default pen&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;The horror of the default
pen&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accessories&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pen&lt;/strong&gt; – The default pen looks like a child’s
whiteboard marker and feels cheap. I replaced it with the &lt;a
href=&quot;https://www.amazon.com/Amazon-Kindle-Scribe-Premium-Improved/dp/B0D2FBN1W8/&quot;&gt;Kindle
Scribe Premium pen&lt;/a&gt;, and the writing experience is vastly
improved.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cover&lt;/strong&gt; – It’s impossible to find a nice cover. I’m
using a $15 cover that I’m encasing in stickers.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool switching&lt;/strong&gt; – Swapping between apps is slow and
clunky. I blame Android and the current limitations of e-ink more than
Boox.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No frontlight&lt;/strong&gt; – The Boox’s lack of frontlight
prevents me from reading more with it. I knew this when I bought my
Boox, but devices with frontlights seem to make other compromises.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Onyx&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Chinese company behind Boox, Onyx International, Inc., runs the
servers where the Boox routes telemetry. I block this traffic with &lt;a
href=&quot;https://pi-hole.net/&quot;&gt;Pi-Hole&lt;/a&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#fn2&quot;
class=&quot;footnote-ref&quot; id=&quot;fnref2&quot;
role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;img
src=&quot;https://photos.tylercipriani.com/2025-03-04_boox-telemetry.png&quot;
alt=&quot;pihole-ing whatever telemetry Boox collects&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;pihole-ing whatever telemetry Boox
collects&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I inspected this traffic via &lt;a href=&quot;https://mitmproxy.org/&quot;&gt;Mitm
proxy&lt;/a&gt;—most traffic was benign, though I never opted into sending any
telemetry (nor am I logged in to a Boox account). But it’s also an
Android device, so it’s feeding telemetry into Google’s gaping maw,
too.&lt;/p&gt;
&lt;p&gt;Worse, &lt;a href=&quot;https://archive.is/HP2Qk&quot;&gt;Onyx is flouting the terms
of the GNU Public License&lt;/a&gt;, declining to release Linux kernel
modifications to users. This is anathema to me—&lt;a
href=&quot;https://www.gnu.org/licenses/gpl-violation.en.html&quot;&gt;GPL
violations&lt;/a&gt; are tantamount to theft.&lt;/p&gt;
&lt;p&gt;Onyx’s disregard for user rights makes me regret buying the Boox.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;verdict&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;Verdict&lt;/h2&gt;
&lt;p&gt;I’ll continue to use the Boox and feel bad about it. I hope my
digging in this post will help the next person.&lt;/p&gt;
&lt;p&gt;Unfortunately, the e-ink tablet market is too niche to support the
kind of solarpunk future I’d always imagined.&lt;/p&gt;
&lt;p&gt;But there’s an opportunity for an open, Linux-based tablet to
dominate e-ink. Linux is playing catch-up on phones with PostmarketOS.
Meanwhile, the best e-ink tablets have to offer are old, unupdateable
versions of Android, like the OS on the Boox.&lt;/p&gt;
&lt;p&gt;In the future, I’d love to pay a license- and privacy-respecting
company for beautiful, calm technology and recommend their product to
everyone. But today is not the future.&lt;/p&gt;
&lt;/section&gt;
&lt;section class=&quot;footnotes footnotes-end-of-document&quot;
role=&quot;doc-endnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn1&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;I go back and forth between “Waking
Up” and “Calm”&lt;a href=&quot;https://tylercipriani.com/blog/#fnref1&quot; class=&quot;footnote-back&quot;
role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id=&quot;fn2&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;Using &lt;a
href=&quot;https://github.com/JordanEJ/Onyx-Boox-Blocklist&quot;&gt;github.com/JordanEJ/Onyx-Boox-Blocklist&lt;/a&gt;&lt;a
href=&quot;https://tylercipriani.com/blog/#fnref2&quot; class=&quot;footnote-back&quot; role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</description>


	<comments>//tylercipriani.com/blog/2025/03/05/boox-go-10-3-review/#comments</comments>

</item>
<item>
	<title>Eventually consistent plain text accounting</title>

	<guid isPermaLink="false">https://tylercipriani.com/blog/2024/10/24/plain-text-accounting/</guid>

	<link>https://tylercipriani.com/blog/2024/10/24/plain-text-accounting/</link>

	<dc:creator>Tyler Cipriani</dc:creator>


	<pubDate>Thu, 24 Oct 2024 00:37:44 +0000</pubDate>
	<dcterms:modified>2024-11-13T02:16:50Z</dcterms:modified>


	<description>&lt;style&gt;.title { text-wrap: balance }&lt;/style&gt;
&lt;figure&gt;
&lt;img src=&quot;https://photos.tylercipriani.com/2024-11-08_expenses.png&quot;
alt=&quot;Spending for October, generated by piping hledger → R&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;Spending for October, generated by piping
hledger → R&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Over the past six months, I’ve tracked my money with &lt;a
href=&quot;https://hledger.org/&quot;&gt;&lt;code&gt;hledger&lt;/code&gt;&lt;/a&gt;—a plain text
double-entry accounting system written in Haskell. It’s been
surprisingly painless.&lt;/p&gt;
&lt;p&gt;My previous attempts to pick up &lt;em&gt;real&lt;/em&gt; accounting tools
floundered. Hosted tools are privacy nightmares, and my stint with &lt;a
href=&quot;https://gnucash.org/&quot;&gt;GnuCash&lt;/a&gt; didn’t last.&lt;/p&gt;
&lt;p&gt;But after stumbling on Dmitry Astapov’s “&lt;a
href=&quot;https://github.com/adept/full-fledged-hledger/wiki/&quot;&gt;Full-fledged
&lt;code&gt;hledger&lt;/code&gt;&lt;/a&gt;” wiki&lt;a href=&quot;https://tylercipriani.com/blog/#fn1&quot; class=&quot;footnote-ref&quot;
id=&quot;fnref1&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;, it
clicked—&lt;strong&gt;eventually consistent&lt;/strong&gt; accounting. Instead of
modeling your money all at once, take it one hacking session at a
time.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It should be easy to work towards eventual consistency. […] I should
be able to [add financial records] bit by little bit, leaving things
half-done, and picking them up later with little (mental) effort.&lt;/p&gt;
&lt;p&gt;– Dmitry Astapov, Full-Fledged Hledger&lt;/p&gt;
&lt;/blockquote&gt;
&lt;section id=&quot;principles-of-my-system&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;Principles of my system&lt;/h2&gt;
&lt;p&gt;I’ve cobbled together a system based on these principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Avoid manual entry&lt;/strong&gt; – Avoid typing in each
transaction. Instead, rely on CSVs from the bank.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSVs as truth&lt;/strong&gt; – CSVs are the only things that
matter. Everything else can be blown away and rebuilt anytime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embrace version control&lt;/strong&gt; – Keep everything under
version control in Git for easy comparison and safe
experimentation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id=&quot;learn-hledger-in-five-minutes&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;Learn &lt;code&gt;hledger&lt;/code&gt; in five minutes&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;hledger&lt;/code&gt; concepts are heady, but its use is simple. I
divide the core concepts into two categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stuff &lt;code&gt;hledger&lt;/code&gt; cares about:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transactions&lt;/strong&gt; – how &lt;code&gt;hledger&lt;/code&gt; moves money
between accounts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Journal files&lt;/strong&gt; – files full of transactions&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Stuff I care about:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rules files&lt;/strong&gt; – how I set up accounts, import CSVs,
and move money between accounts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reports&lt;/strong&gt; – help me see where my money is going and
if I messed up my rules.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Transactions&lt;/strong&gt; move money between accounts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024-01-01 Payday
    income:work      $-100.00
    assets:checking   $100.00&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This transaction shows that on Jan 1, 2024, money moved from
&lt;code&gt;income:work&lt;/code&gt; into &lt;code&gt;assets:checking&lt;/code&gt;—Payday.&lt;/p&gt;
&lt;p&gt;The sum of each transaction should be $0. Money comes from somewhere,
and the same amount goes somewhere else—double-entry accounting. This is
powerful technology—it makes mistakes impossible to ignore.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Journal files&lt;/strong&gt; are text files containing one or more
transactions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024-01-01 Payday
    income:work              $-100.00
    assets:checking           $100.00
2024-01-02 QUANSHENG UVK5
    assets:checking          $-29.34
    expenses:fun:radio        $29.34&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Rules files&lt;/strong&gt; transform CSVs into journal files via
regex matching.&lt;/p&gt;
&lt;p&gt;Here’s a CSV from my bank:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Transaction Date,Description,Category,Type,Amount,Memo
09/01/2024,DEPOSIT Paycheck,Payment,Payment,1000.00,
09/04/2024,PizzaPals Pizza,Food &amp;amp; Drink,Sale,-42.31,
09/03/2024,Amazon.com*XXXXXXXXY,Shopping,Sale,-35.56,
09/03/2024,OBSIDIAN.MD,Shopping,Sale,-10.00,
09/02/2024,Amazon web services,Personal,Sale,-17.89,&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here’s a &lt;code&gt;checking.rules&lt;/code&gt; to transform that CSV into a
journal file so I can use it with &lt;code&gt;hledger&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# checking.rules
# --------------
# Map CSV fields → hledger fields[0]
fields date,description,category,type,amount,memo,_
# `account1`: the account for the whole CSV.[1]
account1    assets:checking
account2    expenses:unknown
skip 1

date-format %m/%d/%Y
currency $

if %type Payment
    account2 income:unknown
if %category Food &amp;amp; Drink
    account2 expenses:food:dining

# [0]: &amp;lt;https://hledger.org/hledger.html#field-names&amp;gt;
# [1]: &amp;lt;https://hledger.org/hledger.html#account-field&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With these two files (&lt;code&gt;checking.rules&lt;/code&gt; and
&lt;code&gt;2024-09_checking.csv&lt;/code&gt;), I can make the CSV into a
journal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ &amp;gt; 2024-09_checking.journal \
    hledger print \
    --rules-file checking.rules \
    -f 2024-09_checking.csv
$ head 2024-09_checking.journal
2024-09-01 DEPOSIT Paycheck
    assets:checking        $1000.00
    income:unknown        $-1000.00

2024-09-02 Amazon web services
    assets:checking          $-17.89
    expenses:unknown          $17.89&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Reports&lt;/strong&gt; are interesting ways to view transactions
between accounts.&lt;/p&gt;
&lt;p&gt;There are registers, balance sheets, and income statements:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ hledger incomestatement \
    --depth=2 \
    --file=2024-09_bank.journal

Revenues:
               $1000.00 income:unknown
-----------------------
               $1000.00


Expenses:
                 $42.31 expenses:food
                 $63.45 expenses:unknown
-----------------------
                $105.76
-----------------------
Net:            $894.24&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the beginning of September, I spent &lt;code&gt;$105.76&lt;/code&gt; and made
&lt;code&gt;$1000&lt;/code&gt;, leaving me with &lt;code&gt;$894.24&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But a good chunk is going to the default expense account,
&lt;code&gt;expenses:unknown&lt;/code&gt;. I can use the
&lt;code&gt;hleger aregister&lt;/code&gt; to see what those transactions are:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ hledger areg expenses:unknown \
    --file=2024-09_checking.journal \
    -O csv | \
  csvcut -c description,change | \
  csvlook
| description              | change |
| ------------------------ | ------ |
| OBSIDIAN.MD              |  10.00 |
| Amazon web services      |  17.89 |
| Amazon.com*XXXXXXXXY     |  35.56 |
l&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, I can add some more rules to my
&lt;code&gt;checking.rules&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if OBSIDIAN.MD
    account2 expenses:personal:subscriptions
if Amazon web services
    account2 expenses:personal:web:hosting
if Amazon.com
    account2 expenses:personal:shopping:amazon&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, I can reprocess my data to get a better picture of my
spending:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ &amp;gt; 2024-09_bank.journal \
    hledger print \
    --rules-file bank.rules \
    -f 2024-09_bank.csv
$ hledger bal expenses \
    --depth=3 \
    --percent \
    -f 2024-09_checking2.journal
              30.0 %  expenses:food:dining
              33.6 %  expenses:personal:shopping
               9.5 %  expenses:personal:subscriptions
              16.9 %  expenses:personal:web
--------------------
             100.0 %&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the Amazon.com purchase, I lumped it into the
&lt;code&gt;expenses:personal:shopping&lt;/code&gt; account. But I could dig
deeper—download &lt;a
href=&quot;https://www.amazon.com/hz/privacy-central/data-requests/preview.html&quot;&gt;my
order history from Amazon&lt;/a&gt; and categorize that spending.&lt;/p&gt;
&lt;p&gt;This is the power of working bit-by-bit—the data guides you to the
next, deeper rabbit hole.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;goals-and-non-goals&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;Goals and non-goals&lt;/h2&gt;
&lt;p&gt;Why am I doing this? For years, I maintained a monthly spreadsheet of
account balances. I had a balance sheet. But I still had questions.&lt;/p&gt;
&lt;figure&gt;
&lt;img
src=&quot;https://photos.tylercipriani.com/2024-11-04_hledger-gnuplot-avg.png&quot;
alt=&quot;Spending over six months, generated by piping hledger → gnuplot&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;Spending over six months, generated by
piping hledger → gnuplot&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Before diving into accounting software, these were my goals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Granular understanding of my spending&lt;/strong&gt; – The big
one. This is where my monthly spreadsheet fell short. I knew I had money
in the bank—I kept my monthly balance sheet. I budgeted up-front the %
of my income I was saving. But I had no idea where my &lt;em&gt;other
money&lt;/em&gt; was going.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data privacy&lt;/strong&gt; – I’m unwilling to hand the keys to my
accounts to YNAB or Mint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Increased value over time&lt;/strong&gt; – The more time I put in,
the more value I want to get out—this is what you get from professional
tools built for nerds. While I wished for low-effort setup, I wanted the
tool to be able to grow to more uses over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Non-goals&lt;/strong&gt;—these are the parts I never cared
about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Investment tracking&lt;/strong&gt; – For now, I left this out of
scope. Between monthly balances in my spreadsheet and online investing
tools’ ability to drill down, I was fine.&lt;a href=&quot;https://tylercipriani.com/blog/#fn2&quot;
class=&quot;footnote-ref&quot; id=&quot;fnref2&quot;
role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Taxes&lt;/strong&gt; – Folks smarter than me help me understand my
yearly taxes.&lt;a href=&quot;https://tylercipriani.com/blog/#fn3&quot; class=&quot;footnote-ref&quot; id=&quot;fnref3&quot;
role=&quot;doc-noteref&quot;&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared system&lt;/strong&gt; – I may want to share reports from
this system, but no one will have to work in it except me.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cash&lt;/strong&gt; – Cash transactions are unimportant to me. I
withdraw money from the ATM sometimes. It evaporates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;hledger&lt;/code&gt; can track all these things. My setup is flexible
enough to support them someday. But that’s unimportant to me right
now.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;monthly-maintenance&quot; class=&quot;level2&quot;&gt;
&lt;h2&gt;Monthly maintenance&lt;/h2&gt;
&lt;p&gt;I spend about an hour a month checking in on my money Which frees me
to spend time making fancy charts—an activity I &lt;a
href=&quot;https://gist.github.com/thcipriani/9cd0a0e1e6d42fe27483d0a84b7de33d&quot;&gt;perversely
enjoy&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;img
src=&quot;http://photos.tylercipriani.com/2024-11-12_hledger-gnuplot-cashflow.png&quot;
alt=&quot;Income vs. Expense, generated by piping hledger → gnuplot&quot; /&gt;
&lt;figcaption aria-hidden=&quot;true&quot;&gt;Income vs. Expense, generated by piping
hledger → gnuplot&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Here’s my setup:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tree ~/Documents/ledger
.
├── export
│   ├── 2024-balance-sheet.txt
│   └── 2024-income-statement.txt
├── import
│   ├── in
│   │   ├── amazon
│   │   │   └── order-history.csv
│   │   ├── credit
│   │   │   ├── 2024-01-01_2024-02-01.csv
│   │   │   ├── ...
│   │   │   └── 2024-10-01_2024-11-01.csv
│   │   └── debit
│   │       ├── 2024-01-01_2024-02-01.csv
│   │       ├── ...
│   │       └── 2024-10-01_2024-11-01.csv
│   └── journal
│       ├── amazon
│       │   └── order-history.journal
│       ├── credit
│       │   ├── 2024-01-01_2024-02-01.journal
│       │   ├── ...
│       │   └── 2024-10-01_2024-11-01.journal
│       └── debit
│           ├── 2024-01-01_2024-02-01.journal
│           ├── ...
│           └── 2024-10-01_2024-11-01.journal
├── rules
│   ├── amazon
│   │   └── journal.rules
│   ├── credit
│   │   └── journal.rules
│   ├── debit
│   │   └── journal.rules
│   └── common.rules
├── 2024.journal
├── Makefile
└── README&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Process:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;&lt;strong&gt;Import&lt;/strong&gt; – download a CSV for the month from each
account and plop it into
&lt;code&gt;import/in/&amp;lt;account&amp;gt;/&amp;lt;dates&amp;gt;.csv&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make&lt;/strong&gt; – run &lt;code&gt;make&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Squint&lt;/strong&gt; – Look at &lt;code&gt;git diff&lt;/code&gt;; if it looks
good, &lt;code&gt;git add . &amp;amp;&amp;amp; git commit -m &quot;💸&quot;&lt;/code&gt; otherwise
review &lt;code&gt;hledger areg&lt;/code&gt; to see details.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;code&gt;Makefile&lt;/code&gt; generates everything under
&lt;code&gt;import/journal&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;journal files from my CSVs using their corresponding rules.&lt;/li&gt;
&lt;li&gt;reports in the &lt;code&gt;export&lt;/code&gt; folder&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I include all the journal files in the &lt;code&gt;2024.journal&lt;/code&gt; with
the line: &lt;code&gt;include ./import/journal/*/*.journal&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here’s the &lt;code&gt;Makefile&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre
class=&quot;sourceCode Makefile&quot;&gt;&lt;code class=&quot;sourceCode makefile&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;SHELL &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;:=&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; /bin/bash&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;RAW_CSV &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;wildcard&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; import/in/**/*.csv&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;JOURNALS &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;foreach&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; file&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt;RAW_CSV&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;subst&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; /in/&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;/journal/&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;patsubst&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; %.csv&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;%.journal&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;))))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-5&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ot&quot;&gt;.PHONY:&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt; all&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-6&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dv&quot;&gt;all:&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt;JOURNALS&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-7&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    hledger is -f 2024.journal &amp;gt; export/2024-income-statement.txt&lt;/span&gt;
&lt;span id=&quot;cb11-8&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    hledger bs -f 2024.journal &amp;gt; export/2024-balance-sheet.txt&lt;/span&gt;
&lt;span id=&quot;cb11-9&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-10&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ot&quot;&gt;.PHONY&lt;/span&gt; &lt;span class=&quot;er&quot;&gt;clean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-11&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dv&quot;&gt;clean:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-12&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;        rm -rf import/journal/**/*.journal&lt;/span&gt;
&lt;span id=&quot;cb11-13&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-14&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dv&quot;&gt;import/journal/%.journal:&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt; import/in/%.csv&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-15&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;ch&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;echo &lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;Processing csv &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$&amp;lt;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; to &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-16&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;ch&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;echo &lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;---&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-17&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;ch&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mkdir -p &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; dirname &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$@)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-18&quot;&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#cb11-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;ch&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hledger print --rules-file rules/&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;st&quot;&gt; basename &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$$&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;(dirname &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$&amp;lt;)&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;)/journal.rules -f &lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$&amp;lt;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt; &amp;gt; &lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If I find anything amiss (e.g., if my balances are different than
what the bank tells me), I look at &lt;code&gt;hleger areg&lt;/code&gt;. I may tweak
my rules or my CSVs and then I run
&lt;code&gt;make clean &amp;amp;&amp;amp; make&lt;/code&gt; and try again.&lt;/p&gt;
&lt;p&gt;Simple, plain text accounting made simple.&lt;/p&gt;
&lt;p&gt;And if I ever want to dig deeper, &lt;code&gt;hledger&lt;/code&gt;’s &lt;a
href=&quot;https://hledger.org/1.40/hledger.html&quot;&gt;docs&lt;/a&gt; have more to
teach. But for now, the balance of effort vs. reward is perfect.&lt;/p&gt;
&lt;/section&gt;
&lt;section class=&quot;footnotes footnotes-end-of-document&quot;
role=&quot;doc-endnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn1&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;while reading a blog post from &lt;a
href=&quot;https://jmtd.net/log/hledger_1yr/&quot;&gt;Jonathan Dowland&lt;/a&gt;&lt;a
href=&quot;https://tylercipriani.com/blog/#fnref1&quot; class=&quot;footnote-back&quot; role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id=&quot;fn2&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;Note, this is covered by &lt;a
href=&quot;https://github.com/adept/full-fledged-hledger/wiki/Investments-easy-approach&quot;&gt;full-fledged
hledger – Investements&lt;/a&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#fnref2&quot; class=&quot;footnote-back&quot;
role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id=&quot;fn3&quot; role=&quot;doc-endnote&quot;&gt;&lt;p&gt;Also covered in &lt;a
href=&quot;https://github.com/adept/full-fledged-hledger/wiki/Tax-returns&quot;&gt;full-fledged
hledger – Tax returns&lt;/a&gt;&lt;a href=&quot;https://tylercipriani.com/blog/#fnref3&quot; class=&quot;footnote-back&quot;
role=&quot;doc-backlink&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</description>


	<comments>//tylercipriani.com/blog/2024/10/24/plain-text-accounting/#comments</comments>

</item>

</channel>
</rss>
