<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ghfs.dev</title>
    <description>The latest articles on DEV Community by ghfs.dev (@ghfs_dev).</description>
    <link>https://dev.to/ghfs_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866800%2F439d56f7-63db-47fe-891b-c28b46356ba1.png</url>
      <title>DEV Community: ghfs.dev</title>
      <link>https://dev.to/ghfs_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ghfs_dev"/>
    <language>en</language>
    <item>
      <title>Every entry twice, then nothing at all: debugging directory listings on macFUSE's FSKit backend</title>
      <dc:creator>ghfs.dev</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:53:38 +0000</pubDate>
      <link>https://dev.to/ghfs_dev/every-entry-twice-then-nothing-at-all-debugging-directory-listings-on-macfuses-fskit-backend-1743</link>
      <guid>https://dev.to/ghfs_dev/every-entry-twice-then-nothing-at-all-debugging-directory-listings-on-macfuses-fskit-backend-1743</guid>
      <description>&lt;p&gt;I build ghfs, a small tool that makes GitHub Issues readable as local files, so a coding agent can read an issue with &lt;code&gt;cat&lt;/code&gt; instead of calling an API mid-task. On Linux it sits on libfuse. On macOS it mounts through macFUSE, using its FSKit backend (&lt;code&gt;-o backend=fskit&lt;/code&gt;), which runs the file system as a File System Extension instead of a kernel extension.&lt;/p&gt;

&lt;p&gt;This post is about one &lt;code&gt;Readdir&lt;/code&gt; function on macOS, three bugs that lived in it, and a root-cause conclusion that I wrote down in April and that turned out to be wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptom 1: every entry, twice
&lt;/h2&gt;

&lt;p&gt;The first report was simple. Run &lt;code&gt;tree&lt;/code&gt; on the mount twice, and the second run lists every entry twice. The first run is correct. Linux never shows it.&lt;/p&gt;

&lt;p&gt;I tried the things you try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;passing sequential offsets to &lt;code&gt;fill&lt;/code&gt; instead of &lt;code&gt;0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;novncache&lt;/code&gt; mount option (ignored by the FSKit backend)&lt;/li&gt;
&lt;li&gt;a stable mtime for directories, which I had been setting to &lt;code&gt;time.Now()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a unique directory handle from &lt;code&gt;Opendir&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;stable inode numbers derived from the path&lt;/li&gt;
&lt;li&gt;going back to the style of cgofuse's &lt;code&gt;memfs&lt;/code&gt; example&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stable mtime was a real fix and made the first run correct. None of the rest changed the second run.&lt;/p&gt;

&lt;p&gt;So I wrote down a root cause: FSKit accumulates readdir results. Return the same entries twice and FSKit unions them. Then I built a workaround on top of that belief:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On macOS, when &lt;code&gt;Readdir&lt;/code&gt; is called with a non-zero offset, return nothing. Treat it as the tail of a listing already served.&lt;/li&gt;
&lt;li&gt;Keep a short-lived cache (500 ms). If the same directory is listed again inside that window, return nothing, so FSKit has nothing to add.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The duplicates went away in the scenario I cared about. One test, &lt;code&gt;tree&lt;/code&gt; twice with nothing else before it, still failed, so I skipped it on macOS and opened a follow-up issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptom 2: nothing at all
&lt;/h2&gt;

&lt;p&gt;Months later, while testing a release, the follow-up turned out to be bigger than &lt;code&gt;tree&lt;/code&gt;. Listing the same directory twice within half a second returned an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;first ls        → test-project
ls after 0.2 s  → (empty)
ls after 0.4 s  → (empty)
ls after 1.9 s  → test-project
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a tool whose readers are coding agents, this is the worst possible failure. An agent that lists the issues directory twice in a row sees no issues, and nothing anywhere reports an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking at what actually arrives
&lt;/h2&gt;

&lt;p&gt;I logged every FUSE call for the root directory during two back-to-back &lt;code&gt;tree&lt;/code&gt; runs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;th&gt;call&lt;/th&gt;
&lt;th&gt;what ghfs did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+0 ms&lt;/td&gt;
&lt;td&gt;Opendir fh=1&lt;/td&gt;
&lt;td&gt;released right away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+5 ms&lt;/td&gt;
&lt;td&gt;Opendir fh=2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+65 ms&lt;/td&gt;
&lt;td&gt;Readdir fh=2 ofst=0&lt;/td&gt;
&lt;td&gt;returned 3 entries (first &lt;code&gt;tree&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+65 ms&lt;/td&gt;
&lt;td&gt;Readdir fh=2 ofst=3&lt;/td&gt;
&lt;td&gt;returned nothing (continuation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+91 ms&lt;/td&gt;
&lt;td&gt;Readdir fh=2 ofst=0&lt;/td&gt;
&lt;td&gt;cache hit, returned nothing (second &lt;code&gt;tree&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out. The second &lt;code&gt;tree&lt;/code&gt; does reach ghfs, so FSKit is not answering from its own cache. And &lt;code&gt;Opendir&lt;/code&gt; is not called again: the second listing reuses the first handle. The macFUSE maintainer has &lt;a href="https://github.com/macfuse/macfuse/issues/819#issuecomment-860068015" rel="noopener noreferrer"&gt;explained why&lt;/a&gt;. The macOS VFS does not pass file or directory handles to file systems, so macFUSE emulates them and reuses them, and two readdirs on the same directory share one handle.&lt;/p&gt;

&lt;p&gt;Then I turned the 500 ms cache off, and the duplicates came back.&lt;/p&gt;

&lt;p&gt;That is the trap. The listing that must get entries (a new &lt;code&gt;tree&lt;/code&gt;) and the listing that, under my theory, had to get nothing arrive in exactly the same shape: same handle, offset 0, tens of milliseconds after the previous one. To avoid the empty case the cache window had to be under 6 ms. To avoid the duplicate case it had to be over 56 ms. No value of the timeout satisfies both, and nothing in the handle, the offset or the open/close sequence tells them apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptom 3, hiding in plain sight
&lt;/h2&gt;

&lt;p&gt;To tell theories apart I wrote a measurement: put N entries in a directory, list it three times, record duplicates and gaps. I expected large directories to show duplication partway through. Instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;entries created&lt;/th&gt;
&lt;th&gt;entries listed&lt;/th&gt;
&lt;th&gt;duplicates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every listing stopped at 63. That was workaround step 1. When the reply buffer fills, the caller comes back with a non-zero offset to get the rest, and I was answering "nothing". Reading &lt;code&gt;readdir&lt;/code&gt; with a non-zero offset is a normal part of the FUSE contract, and returning nothing there is simply wrong. On a real mount, a directory of 344 closed issues listed 255.&lt;/p&gt;

&lt;p&gt;Two of my three bugs were the workaround.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing readdir properly
&lt;/h2&gt;

&lt;p&gt;So I removed both workarounds. &lt;code&gt;Readdir&lt;/code&gt; now builds the full listing, starts from the offset it was given, and passes sequential offsets to &lt;code&gt;fill&lt;/code&gt; so the caller can resume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ofst&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;fillOfst&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOOS&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"darwin"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fillOfst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fillOfst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With macFUSE 5.3.3, large directories now listed completely: 500 entries, three times, no gaps and no duplicates. The offsets ghfs handed out came back unchanged, so the resume path works. But overlapping listings still duplicated. A walk over three issue files returned 27.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment I should have run in April
&lt;/h2&gt;

&lt;p&gt;macFUSE 5.4.0 had just come out, and its release notes say the FSKit backend now improves directory enumeration by "validating cookies and verifiers". I swapped macFUSE on the same machine and ran the same ghfs binary against it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;test&lt;/th&gt;
&lt;th&gt;macFUSE 5.3.3&lt;/th&gt;
&lt;th&gt;macFUSE 5.4.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;500 entries, listed 3×&lt;/td&gt;
&lt;td&gt;500, no duplicates&lt;/td&gt;
&lt;td&gt;500, no duplicates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scenario suite&lt;/td&gt;
&lt;td&gt;fails (3 files → 27)&lt;/td&gt;
&lt;td&gt;passes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tree&lt;/code&gt; twice&lt;/td&gt;
&lt;td&gt;outputs differ&lt;/td&gt;
&lt;td&gt;identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the test skipped since April&lt;/td&gt;
&lt;td&gt;(skipped)&lt;/td&gt;
&lt;td&gt;passes, 3 runs in a row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same directory at 0 ms and 200 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;20 of 20 entries every time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ghfs binary was the same. Only macFUSE changed, and the duplication disappeared. So the duplication was never ghfs, and it was not "FSKit accumulates results" either: FSKit was the same on both runs. It was the translation between FUSE readdir and FSKit's enumeration inside macFUSE, which 5.4.0 changed. The maintainer has also &lt;a href="https://github.com/libfuse/sshfs/pull/371" rel="noopener noreferrer"&gt;described elsewhere&lt;/a&gt; how the FSKit API does not say how many entries it wants, so the backend can end up re-requesting entries it has already returned.&lt;/p&gt;

&lt;p&gt;My April conclusion had a plausible mechanism, and it survived six failed fixes because each fix was tested against it rather than against an alternative. The experiment that could falsify it was cheap: hold my code still and change the layer below. I didn't run it until September.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this did to the requirements
&lt;/h2&gt;

&lt;p&gt;The fix only holds on macFUSE 5.4.0 or later, so that became a requirement. That version also fixes a crash where an empty read reply took down the File System Extension, and ghfs could send one.&lt;/p&gt;

&lt;p&gt;While I was in there, I checked the macOS requirement too. macFUSE ships two FSKit extensions. One is for "local" volumes, meaning physically attached storage, and runs on macOS 15.4 and later, but mounting that way needs an option macFUSE itself calls experimental. The other, for everything else, requires macOS 26. ghfs uses the second one. My docs said "macOS 15.4 or later", and nobody had ever checked it: CI runners cannot enable a File System Extension, because that needs a person to approve it in System Settings.&lt;/p&gt;

&lt;p&gt;So v1.1.0 requires macOS 26 or later and macFUSE 5.4.0 or later. A narrower promise that has been checked is worth more than a wider one that hasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more FSKit lesson: a leftover mount you cannot touch
&lt;/h2&gt;

&lt;p&gt;The same release fixed one more macOS behaviour, which is worth knowing if you build on macFUSE. If the daemon is killed abnormally, macFUSE's file system module keeps holding the volume with nothing behind it. Anything that touches that path, &lt;code&gt;ls&lt;/code&gt; included, enters an uninterruptible wait and cannot be killed. &lt;code&gt;mount&lt;/code&gt;, &lt;code&gt;df&lt;/code&gt; and &lt;code&gt;lsof&lt;/code&gt; walk the mount table and hang the same way. &lt;code&gt;umount&lt;/code&gt; does not return, and &lt;code&gt;umount -f&lt;/code&gt; removes the entry but leaves the module, so mounting at the same path fails.&lt;/p&gt;

&lt;p&gt;Two things do work. &lt;code&gt;getfsstat&lt;/code&gt; with &lt;code&gt;MNT_NOWAIT&lt;/code&gt; returns in about 10 µs and still lists the stale mount, so detection is safe. Sending &lt;code&gt;SIGTERM&lt;/code&gt; to the file system module clears everything, with no reboot.&lt;/p&gt;

&lt;p&gt;What I could not find is a way to tell which module process holds which volume. Quitting the module affects every macFUSE volume on the machine, so ghfs now detects the leftover mount, stops before trying to mount, and prints the command (&lt;code&gt;pkill -f io.macfuse.app.fsmodule.macfuse&lt;/code&gt;) for the user to run. The old code had assumed FSKit unmounts automatically when the process dies. It doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;A workaround built on a wrong root cause spreads. Mine produced two new bugs, and one of them was silent.&lt;/p&gt;

&lt;p&gt;When a symptom might live in the layer below you, hold your code still and change that layer. It is the cheapest experiment you have.&lt;/p&gt;

&lt;p&gt;Write the root cause down as a hypothesis until something has tried to break it.&lt;/p&gt;

&lt;p&gt;On FSKit, &lt;code&gt;readdir&lt;/code&gt; at a non-zero offset is normal. Resume from it.&lt;/p&gt;




&lt;p&gt;ghfs is what all this was for. It prefetches GitHub Issues into a read-only mount so a local coding agent can read them as ordinary files. It needs macOS 26 or later on Apple Silicon with macFUSE 5.4.0 or later (install macFUSE and enable its File System Extension in System Settings before installing ghfs), or Linux on x86_64 or ARM64 with libfuse3, including WSL 2. It is paid and not open source, and the free tier covers one repository: &lt;a href="https://ghfs.dev" rel="noopener noreferrer"&gt;https://ghfs.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>go</category>
      <category>debugging</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a FUSE filesystem that mounts GitHub Issues as local files</title>
      <dc:creator>ghfs.dev</dc:creator>
      <pubDate>Wed, 08 Apr 2026 12:15:55 +0000</pubDate>
      <link>https://dev.to/ghfs_dev/i-built-a-fuse-filesystem-that-mounts-github-issues-as-local-files-33o7</link>
      <guid>https://dev.to/ghfs_dev/i-built-a-fuse-filesystem-that-mounts-github-issues-as-local-files-33o7</guid>
      <description>&lt;p&gt;I usually develop software alongside Generative AI — specifically Claude Code. My workflow revolves around GitHub Issues as units of task management. I have the AI read an issue, investigate the scope, report on the implementation plan, and then proceed with the actual work.&lt;/p&gt;

&lt;p&gt;When passing issue content to the AI, I use the &lt;code&gt;gh&lt;/code&gt; command. The AI "knows" how to use it. But it frequently uses non-existent options or makes incorrect guesses, leading to errors and retries. These are wasted steps.&lt;/p&gt;

&lt;p&gt;This could be addressed with rule files. But rule files should be for project-specific instructions, not for teaching an AI how to use general-purpose CLI tools. That felt like the wrong place to put that effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Just Make It a File
&lt;/h2&gt;

&lt;p&gt;I'm skeptical of MCP servers. They add more uncertainty to the already uncertain behavior of Generative AI.&lt;/p&gt;

&lt;p&gt;A filesystem is the same on every OS. Reading a file, running &lt;code&gt;grep&lt;/code&gt; — these are things even Generative AI can execute reliably.&lt;/p&gt;

&lt;p&gt;I also believe that with the advent of Generative AI, there is an advantage to keeping everything in one place. Having all the information necessary for a project contained within that repository or workspace is beneficial for providing context to the AI.&lt;/p&gt;

&lt;p&gt;So I thought: what if GitHub Issues were visible as local files? That is why I built ghfs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is ghfs?
&lt;/h2&gt;

&lt;p&gt;ghfs is a tool that mounts GitHub Issues as a local filesystem using FUSE. It is written in Go and supports macOS and Linux.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; .ghfs/github/issues/open/
&lt;span class="go"&gt;42.md  108.md  215.md

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .ghfs/github/issues/open/42.md
&lt;span class="go"&gt;---
number: 42
title: "Fix authentication bug"
state: open
author: username
labels:
  - bug
---

Issue body in Markdown...

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="c"&gt;# Comments&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="c"&gt;## username - 2024-01-02T12:00:00Z&lt;/span&gt;
&lt;span class="go"&gt;
Comment content...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made it read-only. This tool is built on the premise that Generative AI will interact with it. I wanted to avoid situations where the AI might update an issue without permission. Even if you set up guardrails, you never know if they will work perfectly. By making it read-only, I can guarantee at the filesystem level that nothing will be destroyed.&lt;/p&gt;

&lt;p&gt;By the way, having issue content show up in an IDE's full-text search is unexpectedly convenient for humans too.&lt;/p&gt;

&lt;p&gt;It does not sync in real-time. Just like how code committed by someone else on GitHub doesn't automatically appear on your local machine, ghfs provides a command to fetch updates manually. A daemon can also sync in the background on a schedule.&lt;/p&gt;

&lt;p&gt;I chose Go because I wanted it to be multi-platform. If I am going to use Generative AI more in the future, the tool should work on Linux and Windows as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Is It Paid?
&lt;/h2&gt;

&lt;p&gt;I have long watched the owners of open-source software become exhausted. I felt that because OSS has become so closely tied to social networking, the purpose and means of a product can become messy.&lt;/p&gt;

&lt;p&gt;And above all, you cannot live without money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Is It For?
&lt;/h2&gt;

&lt;p&gt;People who work based on issues. Or people who can embrace such a workflow.&lt;/p&gt;

&lt;p&gt;It can act as a substitute for a human opening GitHub in a browser, or a substitute for an AI running &lt;code&gt;gh&lt;/code&gt; commands. It comes down to whether you find value in that.&lt;/p&gt;

&lt;p&gt;I would be happy if ghfs helps you in ways I never expected.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://ghfs.dev" rel="noopener noreferrer"&gt;https://ghfs.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;X(Twitter): &lt;a href="https://x.com/ghfsdev" rel="noopener noreferrer"&gt;https://x.com/ghfsdev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
