<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Luke Manning - Blog — Ubuntu</title>
        <link>https://lukemanning.ie/</link>
        <description>Breaking things. Building things. Writing about it. (tag: Ubuntu)</description>
        <lastBuildDate>Wed, 30 Sep 2026 12:46:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, Luke Manning</copyright>
        <atom:link href="https://lukemanning.ie/feeds/ubuntu.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[gh issue view Kept Failing Because Ubuntu's ESM Outranks GitHub's Repo]]></title>
            <link>https://lukemanning.ie/blog/gh-issue-view-failing-ubuntu-deprecation-apt-pin</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/gh-issue-view-failing-ubuntu-deprecation-apt-pin</guid>
            <pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p><strong>TL;DR:</strong> Ubuntu 24.04's packaged gh (2.45.0) is broken. GitHub sunset Projects (classic), and 2.45.0 still requests that field in its GraphQL query, so what reads like a deprecation warning is actually a hard error that kills every <code>gh issue view</code> and <code>gh pr view</code>. Three problems stacked: the warning was the error, GitHub's apt repo lives at <code>/packages</code> not <code>/apt</code>, and Ubuntu ESM out-pins GitHub at priority 510 vs 500. Fixed on 2.97.0.</p>
<hr>
<p>I was trying to read an issue on this site's repo:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> issue</span><span style="color:#9ECBFF"> view</span><span style="color:#79B8FF"> 28</span><span style="color:#79B8FF"> --repo</span><span style="color:#9ECBFF"> ManningWorks/lukemanning-site</span><span style="color:#79B8FF"> --comments</span></span></code></pre>
<p>What came back was this:</p>
<pre><code>GraphQL: Projects (classic) is being deprecated in favor of the new Projects
experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/.
(repository.issue.projectCards)
</code></pre>
<p>I asked my AI assistant (<a href="/blog/anthropic-open-source-walled-garden-clawdbot-opencode">opencode</a>) what it meant. It told me it was just a deprecation warning, not an error, and the command works fine.</p>
<p>Except it kept telling me the command had failed. So I pushed back: "You keep telling me that command fails though." It ran the command, and a different error surfaced. This one was mine. In my message I'd written <code>gh issue view 28 --repo --comments</code> with no repo value, assuming it would fill in the blanks. It ran exactly what I'd written. <code>--repo</code> with no value swallowed <code>--comments</code>:</p>
<pre><code>expected the "[HOST/]OWNER/REPO" format, got "--comments"
</code></pre>
<p>Fine. My shorthand, my missing value. With the repo filled in, the original failure came straight back:</p>
<p>Exit code 1. Zero bytes on stdout. The deprecation message, alone, on stderr.</p>
<p>That's when it clicked. The "warning" was the failure. The command produced nothing except a note formatted like a heads-up about the future.</p>
<p>The assistant then decided the deprecation message was a red herring that would only appear once the repo resolved. Also wrong. Twice in one conversation.</p>
<h2>Problem One: The Warning Was the Error</h2>
<p>Ubuntu ships gh 2.45.0. That version requests <code>repository.issue.projectCards</code> as part of its GraphQL query for issue views. GitHub sunset Projects (classic) in May 2024 and now returns that field as a hard GraphQL error. A hard error kills the whole query, so every <code>gh issue view</code> and <code>gh pr view</code> on 2.45.x fails with output that reads like a deprecation notice.</p>
<p>This is a known thing: <a href="https://github.com/cli/cli/issues/11992">cli/cli#11992</a>. The maintainers' answer is to stop using distro packages and install from GitHub's official repo. GitHub's <a href="https://github.com/cli/cli/blob/trunk/docs/install_linux.md">Linux install docs</a> now say it outright: as of November 2025 they strongly recommend the official Debian packages, since the community-distributed 2.45.x and 2.46.x are broken against current GitHub APIs.</p>
<p>So Ubuntu's gh is broken in a way GitHub officially acknowledges. Weirdly comforting.</p>
<h2>The Workaround</h2>
<p><code>gh api</code> goes through REST, not GraphQL, so it still worked:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> api</span><span style="color:#9ECBFF"> repos/ManningWorks/lukemanning-site/issues/28</span><span style="color:#79B8FF"> --jq</span><span style="color:#9ECBFF"> '.title, .body'</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> api</span><span style="color:#9ECBFF"> repos/ManningWorks/lukemanning-site/issues/28/comments</span><span style="color:#79B8FF"> --jq</span><span style="color:#9ECBFF"> '.[] | .author.login, .body'</span></span></code></pre>
<p>Clunky, but it let me read the issue while I sorted out the upgrade.</p>
<h2>Problem Two: The Wrong Repo URL</h2>
<p>The assistant set up GitHub's apt repo from memory: <code>https://cli.github.com/apt</code>.</p>
<pre><code>The repository 'https://cli.github.com/apt stable Release' does not have a Release file.
</code></pre>
<p>That's a 404. The repo isn't there. I enjoyed this one. The assistant guessed a URL instead of checking the docs, which is the exact thing it tells me not to do constantly.</p>
<p>GitHub's own <a href="https://github.com/cli/cli/blob/trunk/docs/install_linux.md">install docs</a> say it's <code>https://cli.github.com/packages</code>. The assistant followed them this time, keyring staged through /tmp since wget can't write to <code>/etc/apt/keyrings</code> as me:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">wget</span><span style="color:#79B8FF"> -nv</span><span style="color:#79B8FF"> -O</span><span style="color:#9ECBFF"> /tmp/githubcli-archive-keyring.gpg</span><span style="color:#9ECBFF"> https://cli.github.com/packages/githubcli-archive-keyring.gpg</span></span>
<span class="line"><span style="color:#B392F0">cat</span><span style="color:#9ECBFF"> /tmp/githubcli-archive-keyring.gpg</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> sudo</span><span style="color:#9ECBFF"> tee</span><span style="color:#9ECBFF"> /etc/apt/keyrings/githubcli-archive-keyring.gpg</span><span style="color:#F97583"> ></span><span style="color:#9ECBFF"> /dev/null</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> chmod</span><span style="color:#9ECBFF"> go+r</span><span style="color:#9ECBFF"> /etc/apt/keyrings/githubcli-archive-keyring.gpg</span></span>
<span class="line"><span style="color:#79B8FF">echo</span><span style="color:#9ECBFF"> "deb [arch=amd64 signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main"</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> sudo</span><span style="color:#9ECBFF"> tee</span><span style="color:#9ECBFF"> /etc/apt/sources.list.d/github-cli.list</span></span></code></pre>
<p>Repo added, <code>apt update</code> clean, <code>apt install gh</code>... nothing. Still 2.45.0. Three failure layers, for anyone counting.</p>
<h2>Problem Three: ESM Refuses to Lose</h2>
<p><code>apt-cache policy gh</code> explained it:</p>
<pre><code>gh:
  Installed: 2.45.0-1ubuntu0.3+esm3
  Candidate: 2.45.0-1ubuntu0.3+esm3
  Version table:
     2.97.0 500
        500 https://cli.github.com/packages stable/main amd64 Packages
 *** 2.45.0-1ubuntu0.3+esm3 510
        510 https://esm.ubuntu.com/apps/ubuntu noble-apps-security/main amd64 Packages
</code></pre>
<p>GitHub's repo offers 2.97.0 at priority 500, the apt default. Ubuntu ESM pins its version at 510. ESM is Expanded Security Maintenance, the security-update stream that comes with Ubuntu Pro, and this machine is Ubuntu 24.04 (noble), amd64, with Pro enabled. Higher priority wins the candidate slot, even though ESM's 2.45.0 is ancient.</p>
<p>An explicit version bypasses the priority contest:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> gh=</span><span style="color:#79B8FF">2.97.0</span></span></code></pre>
<p>That worked. <code>gh --version</code> reported 2.97.0, and <code>gh issue view 28 --comments</code> (from the repo root, so no <code>--repo</code> needed) finally exited 0 with full output.</p>
<h2>The Pin, Validated</h2>
<p>The explicit install worked, but the candidate was still ESM's 2.45.0. The assistant said I needed a pin file so GitHub's repo wins going forward. Before touching anything in <code>/etc/apt/preferences.d/</code>, I asked it to validate that. Do Ubuntu or GitHub actually document this, or is it forum folklore?</p>
<p>Three primary sources:</p>
<ol>
<li>
<p><strong>My own machine.</strong> <code>/etc/apt/preferences.d/ubuntu-pro-esm-apps</code>, shipped by ubuntu-pro-client, pins <code>release o=UbuntuESMApps</code> at 510. The file's own comment says it's deliberate: when ESM is enabled, ESM packages are preferred over non-ESM ones.</p>
</li>
<li>
<p><strong><code>man apt_preferences</code>.</strong> Priorities from 500 to 990 install a version unless a newer one is already installed. 1000 or higher is required to downgrade. So ESM can't drag me back to 2.45.0, but a plain <code>apt upgrade</code> would never pick up a future GitHub release either. I'd be frozen on 2.97.0.</p>
</li>
<li>
<p><strong>Canonical's tracker and docs.</strong> <a href="https://github.com/canonical/ubuntu-pro-client/issues/3330">ubuntu-pro-client issue #3330</a> is someone hitting exactly this: a newer fish from a PPA, ignored in favour of ESM's. A Canonical maintainer closed it as intended, because cloud customers didn't want ESM security fixes silently reverted. The stated remedy:</p>
<blockquote>
<p>manually configure apt... by creating a preferences file pinning the PPA to a higher version than ESM.</p>
</blockquote>
<p>Canonical's Pro Client docs say close to the same: give the third-party repo at least 510.</p>
</li>
</ol>
<p>So the pin is real, and documented. Good.</p>
<h2>Why 600 and Not 510</h2>
<p>Those two sources don't quite agree, it turns out. The docs allow equal: at least 510. The issue thread says higher than ESM. And what apt actually does when two repos sit at the same priority, I couldn't find in <code>man apt_preferences</code>. Maybe there's a tie-break rule somewhere. I didn't find it.</p>
<p>I let the assistant pick the number in the end, within the bounds the man page had already given me. It went with 600: unconditionally "GitHub's repo is authoritative for gh", above both suggested floors, and below 1000 so nothing can ever downgrade me.</p>
<p>Works for me. 510 would satisfy the docs, but a pin that only ties the thing it's fighting isn't a config I want to reason about again in six months.</p>
<p><code>/etc/apt/preferences.d/gh</code>:</p>
<pre><code># gh: prefer GitHub official apt repo over Ubuntu ESM pin (510).
# ESM gh 2.45.x is broken vs current GitHub APIs - see cli/cli#11992.
# Canonical documents this remedy: https://documentation.ubuntu.com/pro-client/en/latest/explanations/about_esm/
Package: gh
Pin: origin cli.github.com
Pin-Priority: 600
</code></pre>
<p>Then I verified with <code>apt-cache policy gh</code>, which now shows <code>*** 2.97.0 600</code>. I checked because the assistant had flagged the failure mode to watch for: if the origin in the pin doesn't match the host in the sources list, there's no error. The pin just doesn't apply.</p>
<h2>What Stuck</h2>
<p>Any one of these three alone would have been a <a href="/blog/git-corrupt-object-recovery">proper rabbit hole</a>.</p>
<p>The assistant was confidently wrong twice, and that's the part I keep relearning. First that the message was harmless, then that it was a red herring. Both times, actually running the command with stdout and stderr separated settled it in seconds. Asking an AI what a command does is <a href="/blog/premature-optimization-multi-agent-prompts">asking it to hallucinate from training data</a>. The apt URL guess showed exactly how that goes.</p>
<p>gh works now. Which is nice, because I originally just wanted to read one issue.</p>]]></description>
            <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Ubuntu 24.04's packaged gh (2.45.0) is broken. GitHub sunset Projects (classic), and 2.45.0 still requests that field in its GraphQL query, so what reads like a deprecation warning is actually a hard error that kills every <code>gh issue view</code> and <code>gh pr view</code>. Three problems stacked: the warning was the error, GitHub's apt repo lives at <code>/packages</code> not <code>/apt</code>, and Ubuntu ESM out-pins GitHub at priority 510 vs 500. Fixed on 2.97.0.</p>
<hr>
<p>I was trying to read an issue on this site's repo:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> issue</span><span style="color:#9ECBFF"> view</span><span style="color:#79B8FF"> 28</span><span style="color:#79B8FF"> --repo</span><span style="color:#9ECBFF"> ManningWorks/lukemanning-site</span><span style="color:#79B8FF"> --comments</span></span></code></pre>
<p>What came back was this:</p>
<pre><code>GraphQL: Projects (classic) is being deprecated in favor of the new Projects
experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/.
(repository.issue.projectCards)
</code></pre>
<p>I asked my AI assistant (<a href="/blog/anthropic-open-source-walled-garden-clawdbot-opencode">opencode</a>) what it meant. It told me it was just a deprecation warning, not an error, and the command works fine.</p>
<p>Except it kept telling me the command had failed. So I pushed back: "You keep telling me that command fails though." It ran the command, and a different error surfaced. This one was mine. In my message I'd written <code>gh issue view 28 --repo --comments</code> with no repo value, assuming it would fill in the blanks. It ran exactly what I'd written. <code>--repo</code> with no value swallowed <code>--comments</code>:</p>
<pre><code>expected the "[HOST/]OWNER/REPO" format, got "--comments"
</code></pre>
<p>Fine. My shorthand, my missing value. With the repo filled in, the original failure came straight back:</p>
<p>Exit code 1. Zero bytes on stdout. The deprecation message, alone, on stderr.</p>
<p>That's when it clicked. The "warning" was the failure. The command produced nothing except a note formatted like a heads-up about the future.</p>
<p>The assistant then decided the deprecation message was a red herring that would only appear once the repo resolved. Also wrong. Twice in one conversation.</p>
<h2>Problem One: The Warning Was the Error</h2>
<p>Ubuntu ships gh 2.45.0. That version requests <code>repository.issue.projectCards</code> as part of its GraphQL query for issue views. GitHub sunset Projects (classic) in May 2024 and now returns that field as a hard GraphQL error. A hard error kills the whole query, so every <code>gh issue view</code> and <code>gh pr view</code> on 2.45.x fails with output that reads like a deprecation notice.</p>
<p>This is a known thing: <a href="https://github.com/cli/cli/issues/11992">cli/cli#11992</a>. The maintainers' answer is to stop using distro packages and install from GitHub's official repo. GitHub's <a href="https://github.com/cli/cli/blob/trunk/docs/install_linux.md">Linux install docs</a> now say it outright: as of November 2025 they strongly recommend the official Debian packages, since the community-distributed 2.45.x and 2.46.x are broken against current GitHub APIs.</p>
<p>So Ubuntu's gh is broken in a way GitHub officially acknowledges. Weirdly comforting.</p>
<h2>The Workaround</h2>
<p><code>gh api</code> goes through REST, not GraphQL, so it still worked:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> api</span><span style="color:#9ECBFF"> repos/ManningWorks/lukemanning-site/issues/28</span><span style="color:#79B8FF"> --jq</span><span style="color:#9ECBFF"> '.title, .body'</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> api</span><span style="color:#9ECBFF"> repos/ManningWorks/lukemanning-site/issues/28/comments</span><span style="color:#79B8FF"> --jq</span><span style="color:#9ECBFF"> '.[] | .author.login, .body'</span></span></code></pre>
<p>Clunky, but it let me read the issue while I sorted out the upgrade.</p>
<h2>Problem Two: The Wrong Repo URL</h2>
<p>The assistant set up GitHub's apt repo from memory: <code>https://cli.github.com/apt</code>.</p>
<pre><code>The repository 'https://cli.github.com/apt stable Release' does not have a Release file.
</code></pre>
<p>That's a 404. The repo isn't there. I enjoyed this one. The assistant guessed a URL instead of checking the docs, which is the exact thing it tells me not to do constantly.</p>
<p>GitHub's own <a href="https://github.com/cli/cli/blob/trunk/docs/install_linux.md">install docs</a> say it's <code>https://cli.github.com/packages</code>. The assistant followed them this time, keyring staged through /tmp since wget can't write to <code>/etc/apt/keyrings</code> as me:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">wget</span><span style="color:#79B8FF"> -nv</span><span style="color:#79B8FF"> -O</span><span style="color:#9ECBFF"> /tmp/githubcli-archive-keyring.gpg</span><span style="color:#9ECBFF"> https://cli.github.com/packages/githubcli-archive-keyring.gpg</span></span>
<span class="line"><span style="color:#B392F0">cat</span><span style="color:#9ECBFF"> /tmp/githubcli-archive-keyring.gpg</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> sudo</span><span style="color:#9ECBFF"> tee</span><span style="color:#9ECBFF"> /etc/apt/keyrings/githubcli-archive-keyring.gpg</span><span style="color:#F97583"> ></span><span style="color:#9ECBFF"> /dev/null</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> chmod</span><span style="color:#9ECBFF"> go+r</span><span style="color:#9ECBFF"> /etc/apt/keyrings/githubcli-archive-keyring.gpg</span></span>
<span class="line"><span style="color:#79B8FF">echo</span><span style="color:#9ECBFF"> "deb [arch=amd64 signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main"</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> sudo</span><span style="color:#9ECBFF"> tee</span><span style="color:#9ECBFF"> /etc/apt/sources.list.d/github-cli.list</span></span></code></pre>
<p>Repo added, <code>apt update</code> clean, <code>apt install gh</code>... nothing. Still 2.45.0. Three failure layers, for anyone counting.</p>
<h2>Problem Three: ESM Refuses to Lose</h2>
<p><code>apt-cache policy gh</code> explained it:</p>
<pre><code>gh:
  Installed: 2.45.0-1ubuntu0.3+esm3
  Candidate: 2.45.0-1ubuntu0.3+esm3
  Version table:
     2.97.0 500
        500 https://cli.github.com/packages stable/main amd64 Packages
 *** 2.45.0-1ubuntu0.3+esm3 510
        510 https://esm.ubuntu.com/apps/ubuntu noble-apps-security/main amd64 Packages
</code></pre>
<p>GitHub's repo offers 2.97.0 at priority 500, the apt default. Ubuntu ESM pins its version at 510. ESM is Expanded Security Maintenance, the security-update stream that comes with Ubuntu Pro, and this machine is Ubuntu 24.04 (noble), amd64, with Pro enabled. Higher priority wins the candidate slot, even though ESM's 2.45.0 is ancient.</p>
<p>An explicit version bypasses the priority contest:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> gh=</span><span style="color:#79B8FF">2.97.0</span></span></code></pre>
<p>That worked. <code>gh --version</code> reported 2.97.0, and <code>gh issue view 28 --comments</code> (from the repo root, so no <code>--repo</code> needed) finally exited 0 with full output.</p>
<h2>The Pin, Validated</h2>
<p>The explicit install worked, but the candidate was still ESM's 2.45.0. The assistant said I needed a pin file so GitHub's repo wins going forward. Before touching anything in <code>/etc/apt/preferences.d/</code>, I asked it to validate that. Do Ubuntu or GitHub actually document this, or is it forum folklore?</p>
<p>Three primary sources:</p>
<ol>
<li>
<p><strong>My own machine.</strong> <code>/etc/apt/preferences.d/ubuntu-pro-esm-apps</code>, shipped by ubuntu-pro-client, pins <code>release o=UbuntuESMApps</code> at 510. The file's own comment says it's deliberate: when ESM is enabled, ESM packages are preferred over non-ESM ones.</p>
</li>
<li>
<p><strong><code>man apt_preferences</code>.</strong> Priorities from 500 to 990 install a version unless a newer one is already installed. 1000 or higher is required to downgrade. So ESM can't drag me back to 2.45.0, but a plain <code>apt upgrade</code> would never pick up a future GitHub release either. I'd be frozen on 2.97.0.</p>
</li>
<li>
<p><strong>Canonical's tracker and docs.</strong> <a href="https://github.com/canonical/ubuntu-pro-client/issues/3330">ubuntu-pro-client issue #3330</a> is someone hitting exactly this: a newer fish from a PPA, ignored in favour of ESM's. A Canonical maintainer closed it as intended, because cloud customers didn't want ESM security fixes silently reverted. The stated remedy:</p>
<blockquote>
<p>manually configure apt... by creating a preferences file pinning the PPA to a higher version than ESM.</p>
</blockquote>
<p>Canonical's Pro Client docs say close to the same: give the third-party repo at least 510.</p>
</li>
</ol>
<p>So the pin is real, and documented. Good.</p>
<h2>Why 600 and Not 510</h2>
<p>Those two sources don't quite agree, it turns out. The docs allow equal: at least 510. The issue thread says higher than ESM. And what apt actually does when two repos sit at the same priority, I couldn't find in <code>man apt_preferences</code>. Maybe there's a tie-break rule somewhere. I didn't find it.</p>
<p>I let the assistant pick the number in the end, within the bounds the man page had already given me. It went with 600: unconditionally "GitHub's repo is authoritative for gh", above both suggested floors, and below 1000 so nothing can ever downgrade me.</p>
<p>Works for me. 510 would satisfy the docs, but a pin that only ties the thing it's fighting isn't a config I want to reason about again in six months.</p>
<p><code>/etc/apt/preferences.d/gh</code>:</p>
<pre><code># gh: prefer GitHub official apt repo over Ubuntu ESM pin (510).
# ESM gh 2.45.x is broken vs current GitHub APIs - see cli/cli#11992.
# Canonical documents this remedy: https://documentation.ubuntu.com/pro-client/en/latest/explanations/about_esm/
Package: gh
Pin: origin cli.github.com
Pin-Priority: 600
</code></pre>
<p>Then I verified with <code>apt-cache policy gh</code>, which now shows <code>*** 2.97.0 600</code>. I checked because the assistant had flagged the failure mode to watch for: if the origin in the pin doesn't match the host in the sources list, there's no error. The pin just doesn't apply.</p>
<h2>What Stuck</h2>
<p>Any one of these three alone would have been a <a href="/blog/git-corrupt-object-recovery">proper rabbit hole</a>.</p>
<p>The assistant was confidently wrong twice, and that's the part I keep relearning. First that the message was harmless, then that it was a red herring. Both times, actually running the command with stdout and stderr separated settled it in seconds. Asking an AI what a command does is <a href="/blog/premature-optimization-multi-agent-prompts">asking it to hallucinate from training data</a>. The apt URL guess showed exactly how that goes.</p>
<p>gh works now. Which is nice, because I originally just wanted to read one issue.</p>]]></content:encoded>
            <category>github</category>
            <category>ubuntu</category>
        </item>
        <item>
            <title><![CDATA[Dropping Obsidian Sync for Syncthing: What I Learned Setting Up a Headless Linux Sync Hub]]></title>
            <link>https://lukemanning.ie/blog/syncthing-obsidian-vault-sync</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/syncthing-obsidian-vault-sync</guid>
            <pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Obsidian Sync costs up to $120 a year if you're on Sync Plus, and paying monthly. That's the number that made me actually do something about it.</p>
<p>I was curious about using Obsidian Sync because people generally were happy that it worked. At least people on reddit were. However I did not want yet another subscription. As much as I like the work that Obsidian do, I just couldn't justify it.</p>
<p>I went with Syncthing because it's peer-to-peer (no third-party server holding my files), it's free, and I've read several threads and watched a few YouTube videos where people swear by it. My always-on NucBox (Ubuntu 24.04, headless) acts as the hub. Windows desktop and Android phone sync to it. I genuinely forget it's there most of the time.</p>
<p>This post isn't a tutorial. The Syncthing docs are fine. What I kept running into was the specific problem of configuring a headless Linux machine without a browser, and nobody really spelled that part out clearly. So here's my setup, including the bits I had to figure out myself.</p>
<h2>Getting to the Web UI</h2>
<p>Most Syncthing guides assume you can just open <code>localhost:8384</code> in a browser on the same machine running Syncthing. My NucBox doesn't have a browser. Or a screen. It's just a small fanless PC sitting under my desk running Ubuntu without a Desktop environment.</p>
<p>My first thought was to install a VNC server or something. I wasn't a fan of that approach and after some more time Googling around I eventually found the SSH tunnel approach, which is way simpler than I expected.</p>
<p>Syncthing's web UI runs on port 8384 on the NucBox. I mapped that to a local port on my Windows machine over SSH:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">ssh</span><span style="color:#79B8FF"> -L</span><span style="color:#9ECBFF"> 9384:localhost:8384</span><span style="color:#9ECBFF"> luke@</span><span style="color:#F97583">&#x3C;</span><span style="color:#9ECBFF">nucbox-i</span><span style="color:#E1E4E8">p</span><span style="color:#F97583">></span><span style="color:#79B8FF"> -N</span></span></code></pre>
<p>Then opened <code>localhost:9384</code> in my browser on my desktop. Syncthing defaults to no auth on the web UI, and I didn't love the idea of an unauthenticated interface accessible over the network. However given this was all running on localhost, I figured that was a problem for another day.</p>
<p>I was already running Syncthing on Windows, which was using port 8384. That's why I mapped to 9384 instead. Anything unused works.</p>
<h2>What I Ran on the NucBox</h2>
<p>Three commands:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> syncthing</span></span>
<span class="line"><span style="color:#B392F0">systemctl</span><span style="color:#79B8FF"> --user</span><span style="color:#9ECBFF"> enable</span><span style="color:#9ECBFF"> syncthing</span></span>
<span class="line"><span style="color:#B392F0">systemctl</span><span style="color:#79B8FF"> --user</span><span style="color:#9ECBFF"> start</span><span style="color:#9ECBFF"> syncthing</span></span></code></pre>
<p>After those, I checked the service was actually running with <code>systemctl --user status syncthing</code>. Active and running. That was my "okay, it's alive" moment.</p>
<p>Actually, one thing I assumed I'd have to deal with. <code>systemctl --user</code> services on a headless machine don't survive logout unless you enable "lingering." Without it, Syncthing stops the second you close your SSH session. I checked mine:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">loginctl</span><span style="color:#9ECBFF"> show-user</span><span style="color:#9ECBFF"> luke</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> grep</span><span style="color:#9ECBFF"> Linger</span></span></code></pre>
<p>Turns out it was already enabled (<code>Linger=yes</code>). I'm guessing something during the Ubuntu install set it up — I definitely never ran <code>loginctl enable-linger</code> myself. But if yours shows <code>Linger=no</code>, you'll need:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">loginctl</span><span style="color:#9ECBFF"> enable-linger</span><span style="color:#9ECBFF"> luke</span></span></code></pre>
<p>This is probably the most headless-specific gotcha in the whole setup. And I suspect a lot of guides skip it because they assume you have a desktop login session.</p>
<p>The Ubuntu package is fine. I didn't need any third-party repos.</p>
<p>Once the tunnel was up it was the standard Syncthing workflow. I added a folder, pointed it at my vault, and shared it with my other devices. I installed Syncthing-Fork on Android from the F-Droid store. Paired devices using the Device ID from <strong>Actions → Show ID</strong>.</p>
<p>The Android app can scan a QR code from that same screen, which is much easier than typing out a 60-character device ID by hand. It can also discover other Syncthing devices on the same network, which was super useful for me.</p>
<h2>The .stignore Thing</h2>
<p>I'd been running Syncthing for maybe a day when I started seeing orange warning triangles in the UI. Low-level conflicts on <code>.obsidian/workspace.json</code> and <code>.obsidian/workspace-mobile.json</code>. Nothing catastrophic, but it was noise I didn't need.</p>
<p>Turns out Obsidian rewrites those files every time you open it — they store open tabs, cursor positions, that kind of thing. Different on every device, changing constantly. No wonder Syncthing was confused.</p>
<p>A bit of searching told me about <code>.stignore</code>. I added this at the root of the vault:</p>
<pre><code>.obsidian/workspace.json
.obsidian/workspace-mobile.json
.trash/
</code></pre>
<p>The rest of <code>.obsidian/</code> (plugins, themes, settings) syncs fine. That's the stuff I actually want consistent across machines.</p>
<h2>A Few Weeks In</h2>
<p>On the same LAN it syncs in seconds. Over the internet it handles NAT traversal automatically. I didn't have to configure any port forwarding or open firewall ports. The NucBox isn't running ufw, so there was nothing to touch there. It just worked.</p>
<p>Syncthing apparently used to use relay servers for this, but they were removed a while back (I think around v1.27). Whatever it's doing now, I didn't have to think about it.</p>
<p>I've had this running for a few days now across the NucBox, my Windows desktop, and my Android phone. I haven't thought about it once since setting it up, which is exactly what I wanted.</p>
<p>The $120 a year I was considering paying Obsidian Sync? Going toward something else now.</p>]]></description>
            <content:encoded><![CDATA[<p>Obsidian Sync costs up to $120 a year if you're on Sync Plus, and paying monthly. That's the number that made me actually do something about it.</p>
<p>I was curious about using Obsidian Sync because people generally were happy that it worked. At least people on reddit were. However I did not want yet another subscription. As much as I like the work that Obsidian do, I just couldn't justify it.</p>
<p>I went with Syncthing because it's peer-to-peer (no third-party server holding my files), it's free, and I've read several threads and watched a few YouTube videos where people swear by it. My always-on NucBox (Ubuntu 24.04, headless) acts as the hub. Windows desktop and Android phone sync to it. I genuinely forget it's there most of the time.</p>
<p>This post isn't a tutorial. The Syncthing docs are fine. What I kept running into was the specific problem of configuring a headless Linux machine without a browser, and nobody really spelled that part out clearly. So here's my setup, including the bits I had to figure out myself.</p>
<h2>Getting to the Web UI</h2>
<p>Most Syncthing guides assume you can just open <code>localhost:8384</code> in a browser on the same machine running Syncthing. My NucBox doesn't have a browser. Or a screen. It's just a small fanless PC sitting under my desk running Ubuntu without a Desktop environment.</p>
<p>My first thought was to install a VNC server or something. I wasn't a fan of that approach and after some more time Googling around I eventually found the SSH tunnel approach, which is way simpler than I expected.</p>
<p>Syncthing's web UI runs on port 8384 on the NucBox. I mapped that to a local port on my Windows machine over SSH:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">ssh</span><span style="color:#79B8FF"> -L</span><span style="color:#9ECBFF"> 9384:localhost:8384</span><span style="color:#9ECBFF"> luke@</span><span style="color:#F97583">&#x3C;</span><span style="color:#9ECBFF">nucbox-i</span><span style="color:#E1E4E8">p</span><span style="color:#F97583">></span><span style="color:#79B8FF"> -N</span></span></code></pre>
<p>Then opened <code>localhost:9384</code> in my browser on my desktop. Syncthing defaults to no auth on the web UI, and I didn't love the idea of an unauthenticated interface accessible over the network. However given this was all running on localhost, I figured that was a problem for another day.</p>
<p>I was already running Syncthing on Windows, which was using port 8384. That's why I mapped to 9384 instead. Anything unused works.</p>
<h2>What I Ran on the NucBox</h2>
<p>Three commands:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> syncthing</span></span>
<span class="line"><span style="color:#B392F0">systemctl</span><span style="color:#79B8FF"> --user</span><span style="color:#9ECBFF"> enable</span><span style="color:#9ECBFF"> syncthing</span></span>
<span class="line"><span style="color:#B392F0">systemctl</span><span style="color:#79B8FF"> --user</span><span style="color:#9ECBFF"> start</span><span style="color:#9ECBFF"> syncthing</span></span></code></pre>
<p>After those, I checked the service was actually running with <code>systemctl --user status syncthing</code>. Active and running. That was my "okay, it's alive" moment.</p>
<p>Actually, one thing I assumed I'd have to deal with. <code>systemctl --user</code> services on a headless machine don't survive logout unless you enable "lingering." Without it, Syncthing stops the second you close your SSH session. I checked mine:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">loginctl</span><span style="color:#9ECBFF"> show-user</span><span style="color:#9ECBFF"> luke</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> grep</span><span style="color:#9ECBFF"> Linger</span></span></code></pre>
<p>Turns out it was already enabled (<code>Linger=yes</code>). I'm guessing something during the Ubuntu install set it up — I definitely never ran <code>loginctl enable-linger</code> myself. But if yours shows <code>Linger=no</code>, you'll need:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">loginctl</span><span style="color:#9ECBFF"> enable-linger</span><span style="color:#9ECBFF"> luke</span></span></code></pre>
<p>This is probably the most headless-specific gotcha in the whole setup. And I suspect a lot of guides skip it because they assume you have a desktop login session.</p>
<p>The Ubuntu package is fine. I didn't need any third-party repos.</p>
<p>Once the tunnel was up it was the standard Syncthing workflow. I added a folder, pointed it at my vault, and shared it with my other devices. I installed Syncthing-Fork on Android from the F-Droid store. Paired devices using the Device ID from <strong>Actions → Show ID</strong>.</p>
<p>The Android app can scan a QR code from that same screen, which is much easier than typing out a 60-character device ID by hand. It can also discover other Syncthing devices on the same network, which was super useful for me.</p>
<h2>The .stignore Thing</h2>
<p>I'd been running Syncthing for maybe a day when I started seeing orange warning triangles in the UI. Low-level conflicts on <code>.obsidian/workspace.json</code> and <code>.obsidian/workspace-mobile.json</code>. Nothing catastrophic, but it was noise I didn't need.</p>
<p>Turns out Obsidian rewrites those files every time you open it — they store open tabs, cursor positions, that kind of thing. Different on every device, changing constantly. No wonder Syncthing was confused.</p>
<p>A bit of searching told me about <code>.stignore</code>. I added this at the root of the vault:</p>
<pre><code>.obsidian/workspace.json
.obsidian/workspace-mobile.json
.trash/
</code></pre>
<p>The rest of <code>.obsidian/</code> (plugins, themes, settings) syncs fine. That's the stuff I actually want consistent across machines.</p>
<h2>A Few Weeks In</h2>
<p>On the same LAN it syncs in seconds. Over the internet it handles NAT traversal automatically. I didn't have to configure any port forwarding or open firewall ports. The NucBox isn't running ufw, so there was nothing to touch there. It just worked.</p>
<p>Syncthing apparently used to use relay servers for this, but they were removed a while back (I think around v1.27). Whatever it's doing now, I didn't have to think about it.</p>
<p>I've had this running for a few days now across the NucBox, my Windows desktop, and my Android phone. I haven't thought about it once since setting it up, which is exactly what I wanted.</p>
<p>The $120 a year I was considering paying Obsidian Sync? Going toward something else now.</p>]]></content:encoded>
            <category>homelab</category>
            <category>obsidian</category>
            <category>ubuntu</category>
        </item>
    </channel>
</rss>