Your entire GitHub, as a folder.
ghfs mounts every repository your token can see as a single
filesystem on Linux. Browse it with ls, open files in your editor,
read what you need on demand. Every tool that takes a path just works,
across every repo you can reach. When you actually want to change something,
ghfs promote flips one repo into a real on-disk git clone in-place,
so vim, git commit, and git push flow straight through the mount.
~/ghfs/
abdulrahman1s/
github-fs/
Cargo.toml
README.md
src/main.rs
rust-lang/
rust/
...
gists/
aa5a315d61ae9438b18d/
hello.py
notes.md
# Find every service in your org with a Dockerfile, in one command.
fd Dockerfile ~/ghfs/myorg
# Read a file from any repo, without cloning it.
cat ~/ghfs/torvalds/linux/MAINTAINERS
# Open a repo in your editor straight from the mount.
code ~/ghfs/rust-lang/rust
# Spot a bug? Promote in place, edit, commit, push.
ghfs promote ~/ghfs/myorg/api
$EDITOR ~/ghfs/myorg/api/src/server.rs
cd ~/ghfs/myorg/api && git commit -am 'fix it' && git pushThe last block is the trick that sets ghfs apart.
~/ghfs/myorg/api is the same path before and after ghfs promote: same shell cwd, same open editor buffers, same inode. It
just becomes writable, backed by a real git checkout.
- You work across a lot of repos. An org with dozens of services,
a personal account with years of side projects, or just open source
you keep cloning into
~/codeand forgetting about. - You live in a terminal.
fd,fzf,vim/nvim,bat, anything that consumes paths is now a multi-repo tool. (Avoid tools that bulk-read file contents across the mount — every uncached file is a GitHub API round-trip; reach forghfs promotefirst if you want to grep a whole repo.) - You want to read code without ceremony. Skim a dependency's source, look up how an upstream project handles something, share a path with a colleague. No "let me clone it first."
- You want one path for the whole workflow. Read, realize you need to fix it, edit and commit, without ever changing directories or re-cloning.
- One mount, every repo. No per-repo
git clone, no remembering which checkout lives where. Repos land under<mount>/<owner>/<repo>/. - First read fetches; the rest is local. Files are cached on disk
after first access and re-validated with ETags, so re-reads don't
burn your GitHub rate limit. Wipe
~/.cache/ghfs/any time to start fresh; nothing is lost. - Read-only by default; writable where it matters. Edits return
EROFSeverywhere except inside a repo you'veghfs promote'd. Inside that repo, ops pass through to a real working tree, sovim,git status,git commit, andgit pushall work through the mount. - One branch per repo dir (the GitHub default by default). Swap
per-repo with
ghfs branch <path> <other>. Promoted repos clone every branch, configureorigin, and track upstream branches —cdin andgit checkout <other>to switch what the mount serves. - Filter what shows up. Yourself, everything visible, or an owner allowlist. Hide forks; show only private or only public. See DOCS.md.
- Gists too. A
gists/directory at the mount root lists all your GitHub gists (public and secret), each as a directory of its files. Filter by visibility (gists_visibility = "public"/"secret"); disable entirely withgists_path = "". See DOCS.md. - Clone-on-demand. Let
ghfsauto-promote repos the first time you touch them, no manual step. See DOCS.md.
Install the latest release to ~/.local/bin/ghfs:
curl -fsSL https://raw.githubusercontent.com/abdulrahman1s/github-fs/master/install.sh | shThe installer also drops bash/zsh/fish completions into the standard
XDG paths (opt out with --no-completions).
See DOCS.md for installer flags (--yes,
--no-modify-rc, --no-completions, per-shell PATH-export overrides)
and other install methods.
Build from source:
git clone https://github.com/abdulrahman1s/github-fs.git
cd github-fs
cargo build --release
install -m 0755 target/release/ghfs ~/.local/bin/ghfsYou will also need fusermount3 and the kernel FUSE module. Debian/Ubuntu:
sudo apt install fuse3. Fedora/Arch/Alpine: package fuse3.
NixOS users
Use the flake instead of a manual install:
nix run github:abdulrahman1s/github-fs#ghfs -- --help
nix profile install github:abdulrahman1s/github-fs#ghfsFor the NixOS module (with optional systemd user-service for auto-mount) and the prebuilt-release option, see DOCS.md.
# point ghfs at a GitHub personal access token
export GHFS_TOKEN=ghp_xxx
# or
mkdir -p ~/.config/ghfs
echo 'token = "ghp_xxx"' > ~/.config/ghfs/config.toml
# smoke-test auth
ghfs whoami
# mount
mkdir -p ~/ghfs
ghfs mount ~/ghfs
# in another shell:
ls ~/ghfs
ls ~/ghfs/<owner>
ls ~/ghfs/<owner>/<some-repo>
cat ~/ghfs/<owner>/<some-repo>/README.md
# switch which branch <some-repo> shows (takes effect on next mount)
ghfs branch ~/ghfs/<owner>/<some-repo> dev
# Ctrl-C in the mount terminal to unmount, or from another shell:
ghfs unmount ~/ghfs
# list active ghfs mounts
ghfs status
# force-refresh the cached repo list and show added/removed repos
# — also signals every running mount via SIGUSR1 to pick up the change in place
ghfs refreshToken scopes: repo for private repos, none for public ones.
| Command | What it does |
|---|---|
ghfs whoami |
Print the authenticated GitHub user. Smoke-tests auth. |
ghfs mount <path> |
Mount the GitHub filesystem at <path> (foreground). |
ghfs unmount <path> [--strict] |
Unmount via fusermount3 -uz (lazy by default — detaches a busy mount and frees it once the last reference drops). Pass --strict to refuse on busy and surface the holder PIDs instead. |
ghfs status |
List active ghfs mounts (scans /proc/mounts). |
ghfs refresh |
Re-fetch the cached repo list and show added/removed repos. |
ghfs info <path> |
Print repo metadata (URL, description, visibility, fork flag, default/effective branch) for the repo at <path> inside an active mount. |
ghfs list |
List all locally cloned repos with their checked-out branch and disk size. |
ghfs config |
Print the resolved configuration (effective values after merging config file, env vars, and defaults). Shows token source without revealing the value. |
ghfs promote <path> [--branch B] |
Manually clone a repo into a local working copy (origin configured, every branch fetched, --branch initially checked out). Works regardless of [clone] trigger. <path> is a path inside an active mount, e.g. ~/ghfs/<owner>/<repo>. |
ghfs rm <owner>/<repo> |
Remove a locally cloned repo from the clone store, freeing disk space. Also accepts a FUSE mount path. DB metadata (branch overrides, cache) is preserved. Idempotent. |
ghfs branch <path> <B> |
Set which branch the mount surfaces under <mount>/<owner>/<repo>/. <path> is a path inside an active mount. Persistent; applies on next mount. Pass --default to clear. |
ghfs completions <shell> |
Print a shell-completion script (bash, zsh, fish, elvish, powershell) to stdout. Redirect into the location your shell expects. |
See DOCS.md for the full layout, mount semantics, configuration, errno mapping, systemd auto-mount, caching internals, and development workflows.
- Read-only by default; writable under materialized repos. Writes
outside a materialized repo return
EROFS. Inside one, ops pass through to the on-disk working tree. - Two-level layout. Repos live under
<mount>/<owner>/<repo>/. - One branch per repo dir.
~/ghfs/<owner>/<repo>/is the repo's effective branch (override fromghfs branch, falling back to the GitHub default). Override changes take effect at the next mount. - Repo dirs carry real timestamps. A repo directory's
stattimes reflect GitHub activity —mtimefrom the last push,crtimefrom creation — sols -lat <mount>/<owner>sorts your repos by recent activity. Materialized repos report on-disk times instead. - Symlinks (
mode 120000) are surfaced as real symlinks. - Hard links.
link(2)works inside a single materialized repo+branch; crossing worktrees returnsEXDEV, linking into or out of a virtual path returnsEROFS. Each name gets its own FUSE inode number, sost_nlinkis accurate butst_ino-based dedup (du,tar -l,rsync -H) doesn't recognize the link. - Submodules (
mode 160000) show as empty directories; gitlinks aren't followed. - Truncated trees (>~100k entries or >7 MB) log a warning and may omit some entries; promote the repo to read it in full.
GitHub errors are translated to errnos at the FUSE boundary:
| Cause | errno |
|---|---|
| 401 Unauthorized / 403 Forbidden (no rate-limit) | EACCES |
403 with X-RateLimit-Remaining: 0 |
EAGAIN |
| 404 Not Found | ENOENT |
| Network / 5xx / decode failure | EIO |
Run with RUST_LOG=ghfs=debug for verbose op tracing.
See DOCS.md for installation variants, configuration, mount semantics, caching internals, systemd auto-mount, privacy/security notes, and development workflows.
MIT.