ghfs

Security and data handling

Last verified against ghfs v1.1.0 on 20 September 2026. We re-check this page when we publish a new version.

ghfs runs on your machine and reads your GitHub issues. Before you point it at a private repository, here is exactly what it asks for, what it stores, where it stores it, and what leaves your machine.

On this page

  1. What ghfs asks GitHub for
  2. What is stored, and where
  3. What leaves your machine
  4. What "read-only" means here
  5. Releases, checksums, and signatures
  6. Reporting a security problem
  7. Website analytics
  8. What we have not done

1. What ghfs asks GitHub for

By default ghfs init authenticates through GitHub's device flow as an OAuth App and requests three scopes:

Scope What GitHub grants
repo Full access to public and private repositories, including read and write access to code
read:org Read-only access to organization membership, organization projects, and team membership
read:project Read-only access to user and organization projects

repo is broader than what ghfs uses. These are the only GitHub API calls in ghfs:

  • list issues in a repository, including checking the latest issue-tracker activity
  • fetch a single issue
  • list comments on an issue
  • read your remaining API rate limit
  • read your own username, to name the profile

There are no write calls. But GitHub's classic OAuth scopes have no "read issues in private repositories" scope, so repo is what an OAuth App has to ask for to reach issues in a private repository. We would rather tell you that than round it off.

Narrowing it yourself

You do not have to use the device flow. ghfs accepts a token from three other places, and each works with a fine-grained personal access token scoped to Issues: Read-only:

auth_method Where the token comes from
token written directly in ~/.ghfs/profiles.yaml
token_command the standard output of a command you specify
env an environment variable you name

Set this in ~/.ghfs/profiles.yaml. With a fine-grained token, GitHub itself refuses any write, rather than ghfs choosing not to make one. The permissions you need are Issues: Read-only and Metadata: Read — nothing else.

One trade-off worth knowing before you choose: a fine-grained token reaches one account or organization, so working across several means one token each. ghfs handles that — define a profile per token in profiles.yaml and assign profiles per project — but it is more setup than the device flow, which authorises everything you can already reach in one step. Organizations also have to allow fine-grained tokens before one will work there.


2. What is stored, and where

The default directory for ghfs data files is ~/.ghfs (or the directory you set in GHFS_HOME). Device-flow tokens may instead use the OS keychain, and the log path is configurable.

Your GitHub token

When you use the device flow, the token is stored in your operating system's keychain when one is reachable, under the service name ghfs. On machines without a reachable keychain — a headless Linux box or WSL 2 without a running Secret Service — ghfs falls back to a file:

~/.ghfs/.secrets/github/<username>     mode 0600, directory mode 0700

In the fallback the token is stored as plain JSON. It is not encrypted; the protection is the file mode. Tokens supplied through token, env, or token_command instead come from the source you configured; the keychain fallback described here is for device flow.

The token is sent only to github.com and api.github.com. It is never sent to our servers.

Issue data

Issues and comments are cached as the JSON GitHub returned:

~/.ghfs/shadow/<provider>/<owner>/<repo>/<service>/

ghfs requests mode 0644 for cache files and 0755 for their directories. Effective access also depends on your umask, parent-directory permissions, and ACLs. Another account that can traverse those directories may be able to read private issue titles, bodies, and comments. On a shared machine, restrict access to the directory you use for GHFS_HOME; treat the cache as you would a local clone of a private repository.

Cached issue data is never sent anywhere. Our servers never receive it.

Your device identifier

A random UUIDv7, generated on your machine the first time a licence is activated and stored in ~/.ghfs/device_id (mode 0600). It is not derived from your hardware, hostname, or username.

Local operational records

The daemon records mount attempts in ~/.ghfs/mount-attempts.json (mode 0600): failure count, timestamps, and the last failure reason. Its default log is ~/.ghfs/logs/daemon.log; log_file can select another path. Logs can contain project names, local paths, and error details. New log files use mode 0600. By default the log rotates at 10 MiB and keeps three backups. ghfs does not upload these records.


3. What leaves your machine

With the production defaults, ghfs contacts these three hosts. The licence and release URLs are configurable. A token_command you supply may make its own connections.

Host Why When
github.com device flow authorisation and token issuance ghfs init, and again if the token expires
api.github.com reading issues, comments, issue-tracker activity, rate limit on demand, and on the polling interval (5 minutes by default)
ghfs.dev licence activation, validation, refresh, deactivation; ghfs update see below

There is no analytics or telemetry SDK in ghfs.

What licence requests carry

Activation, checking, and refresh include a deviceInfo object with these five fields:

  • deviceId
  • os — for example darwin, linux
  • arch — for example arm64, amd64
  • ghfsVersion
  • publicKeyVersion — which signing key your copy verifies licences against

Activation uses the random device identifier described above. In v1.1.0, the daemon's check and refresh requests put the licence ID in deviceInfo.deviceId instead.

These fields are not the whole request. Activation and refresh also send your licence key. Checking also sends your licence ID and public-key version. Deactivation sends only the licence ID and device ID; v1.1.0 puts the licence ID in both fields.

No hostname, no MAC address, no username, no repository names, no GitHub token.

Request When
activate you run ghfs license activate
check while the daemon runs, roughly every 10 hours
refresh when a check says the licence needs refreshing
deactivate you run ghfs license deactivate

On the free plan the daemon makes none of these requests. The periodic check only starts when a paid licence is present.

Update checks

ghfs update contacts ghfs.dev when you run it. Nothing checks for updates in the background.

What our server records

Licence operations are written to an audit log holding the licence ID, the email address on the licence, the plan tier, and the device identifier. IP addresses are not written to that log. These records are kept for 12 months.

Your network connection is still visible to our hosting and delivery providers at the transport level, as it is for any website. We have not audited what those providers retain by default, and we do not claim otherwise.


4. What "read-only" means here

Two things enforce it, and both are in ghfs:

The filesystem rejects writes. Opening a file for anything other than reading returns EROFS, and so do write, create, mkdir, mknod, unlink, rmdir, rename, truncate, chmod, chown, link, symlink, and utimens. There is no code path that writes into the mount.

ghfs never calls a GitHub write API. The five calls listed in section 1 are the whole set.

Here is what that does not cover. If you authenticate with the device flow, the token ghfs holds has repo, and repo can write. Read-only holds because ghfs does not make those calls, not because GitHub would refuse them. Any other program that reads that token could write with it.

If you want GitHub itself to enforce the restriction, use a fine-grained token as described in section 1.


5. Releases, checksums, and signatures

Every release publishes:

  • one .tar.gz per platform (darwin/arm64, linux/amd64, linux/arm64)
  • ghfs_<version>_checksums.txt
  • ghfs_<version>_checksums.txt.sig — a detached GPG signature over the checksums file

The install script

install.sh verifies the SHA-256 of the archive against the checksums file, then verifies the GPG signature on the checksums file. It fetches our public key, checks its fingerprint against a value hard-coded in the script, and verifies in a temporary keyring so your ~/.gnupg is untouched. A fingerprint mismatch or a failed signature stops the install.

5775 CCF3 50AB 2C8D 2800  C4C2 73B5 8B4B A9F5 7FD8

The same fingerprint is printed on our download page. Compare the two before you trust either.

The script warns and continues rather than stopping in four cases: the checksums file cannot be downloaded, neither sha256sum nor shasum is installed, the checksums file has no entry for your archive, or gpg is not installed. Watch the output. If you want the signature checked, install GnuPG first.

ghfs update

ghfs update downloads the archive and the checksums file and verifies the SHA-256. It does not download or verify the signature. The checksums file comes from the same place as the archive, so this detects a corrupted download but not a compromised release.

To verify a release yourself:

curl -fsSL https://ghfs.dev/gpg-public-key.asc | gpg --import
gpg --fingerprint 5775CCF350AB2C8D2800C4C273B58B4BA9F57FD8
gpg --verify ghfs_<version>_checksums.txt.sig ghfs_<version>_checksums.txt
sha256sum -c ghfs_<version>_checksums.txt

On macOS, use shasum -a 256 -c for the last line.


6. Reporting a security problem

Write to contact@ghfs.dev.

We do not run a dedicated security reporting channel yet, and the source repository is not public, so there is no issue tracker to file against. One person reads that mailbox and is not on call, so we will not promise a response time. We will tell you what we find.


7. Website analytics

Our content delivery provider collects aggregate analytics for this website. Its automatic setup adds the script at the edge, so it is absent from our site's source. The provider states that it uses no cookies or other client-side state for analytics and does not track users across sites. The recorded dimensions include page address, referring site, country, device type, browser, and operating system. Records older than about a week are kept only as a sample. None of this applies to ghfs itself, which contains no analytics.


8. What we have not done

We would rather list this than let the page imply it.

  • ghfs has not been audited by a third party
  • we do not publish a software bill of materials
  • our source is not public
  • we have not audited what our hosting and delivery providers retain by default

Because the source is closed, you cannot check the claims on this page by reading the code. What you can check independently is the signature on each release, and the traffic ghfs sends from your own machine.