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
- What ghfs asks GitHub for
- What is stored, and where
- What leaves your machine
- What "read-only" means here
- Releases, checksums, and signatures
- Reporting a security problem
- Website analytics
- 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:
deviceIdos— for exampledarwin,linuxarch— for examplearm64,amd64ghfsVersionpublicKeyVersion— 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.gzper platform (darwin/arm64,linux/amd64,linux/arm64) ghfs_<version>_checksums.txtghfs_<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.