⚠ Playground / personal use only. This is a hobby project, not a hardened service. There is no authentication, no rate limiting, and no security audit. Anyone who can reach the port can read every file under the served directory. Run it on your own machine bound to loopback (the default), or on a trusted LAN — never expose it to the public internet, behind a tunnel, or on a shared host.
A single-binary local directory browser — point it at a folder, open the URL, read everything in place from your phone on the same Wi-Fi.
Sometimes you just want to read the markdown notes, text files, and images on your laptop from the couch — on a phone, in a browser, without copying anything anywhere. explorer is a single static Go binary that serves a chosen directory over HTTP as a read-only browser, with markdown rendering, inline images, and deep-linkable URLs. No accounts, no writes, no cloud.
Bare binary, no extract step. Replace <TAG> with the latest release version from the Releases page (e.g. v0.2.0).
# linux/amd64
curl -L -o explorer https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_linux_amd64 && chmod +x explorer && ./explorer ./
# linux/arm64
curl -L -o explorer https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_linux_arm64 && chmod +x explorer && ./explorer ./
# darwin/amd64 (Intel Macs)
curl -L -o explorer https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_darwin_amd64 && chmod +x explorer && ./explorer ./
# darwin/arm64 (Apple Silicon)
curl -L -o explorer https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_darwin_arm64 && chmod +x explorer && ./explorer ./Windows (PowerShell):
Invoke-WebRequest -Uri https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_windows_amd64.exe -OutFile explorer.exe
.\explorer.exe .Same binary inside an archive. The unix archive preserves the executable bit, so no chmod is needed after extracting.
# linux/amd64
curl -L -o explorer.tar.gz https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_linux_amd64.tar.gz && tar -xzf explorer.tar.gz && ./explorer ./
# linux/arm64
curl -L -o explorer.tar.gz https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_linux_arm64.tar.gz && tar -xzf explorer.tar.gz && ./explorer ./
# darwin/amd64
curl -L -o explorer.tar.gz https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_darwin_amd64.tar.gz && tar -xzf explorer.tar.gz && ./explorer ./
# darwin/arm64
curl -L -o explorer.tar.gz https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_darwin_arm64.tar.gz && tar -xzf explorer.tar.gz && ./explorer ./Windows (PowerShell):
Invoke-WebRequest -Uri https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_windows_amd64.zip -OutFile explorer.zip
Expand-Archive .\explorer.zip -DestinationPath .
.\explorer.exe .Each release ships a single checksums.txt covering both bare binaries and archives. --ignore-missing lets the verify step succeed for whichever subset you actually downloaded.
# linux
sha256sum --ignore-missing -c checksums.txt
# macOS
shasum -a 256 --ignore-missing -c checksums.txtAfter running the binary, confirm what you have with ./explorer --version — it prints the embedded tag, short commit, and build date in <tag> (commit <short-sha>, built <yyyy-mm-dd>) form.
macOS Gatekeeper: the first launch may show "cannot be opened because the developer cannot be verified" — these binaries are not signed/notarised. Clear the quarantine attribute once with:
xattr -d com.apple.quarantine ./explorerThis applies to both the bare binary and the binary extracted from the archive.
Windows SmartScreen: the first launch may show "Windows protected your PC". Click More info → Run anyway. Same caveat applies to both shapes — the binaries are not Authenticode-signed.
Requires Go 1.24+.
go install github.com/anantadwi13/explorer/cmd/explorer@latestThis drops an explorer binary in $GOBIN (or $GOPATH/bin — make sure it is on your PATH). Pin a specific version with @v0.3.0 instead of @latest if you prefer.
To try it without installing:
go run github.com/anantadwi13/explorer/cmd/explorer@latest <dir>Requires Go 1.24+ and Node 18+.
git clone https://github.com/anantadwi13/explorer.git
cd explorer
make build # builds the SPA, embeds it, produces ./explorer
./explorer <dir>docker compose up --buildThen open http://localhost:8080. The docker-compose.yml bind-mounts the current directory into /data (read-only) and runs:
explorer /data --host 0.0.0.0 --port 8080
--host 0.0.0.0 is mandatory inside the container. With the default 127.0.0.1, the server listens only on the container's loopback interface and Docker's port mapping cannot reach it.
# Serve the current directory on the default loopback port
explorer .
# Serve /home/alice/docs on port 9000
explorer /home/alice/docs --port 9000
# Expose on all interfaces (LAN-accessible — read the warning below)
explorer . --host 0.0.0.0When --host is not a loopback address, the startup banner prints a warning:
Output:
explorer (playground build — no auth, no audit; do not expose publicly)
explorer serving /home/alice/docs
→ http://0.0.0.0:8080
⚠ host is not loopback — anyone on this network can read these files.
| Flag | Default | Description |
|---|---|---|
--port |
8080 |
TCP port to listen on |
--host |
127.0.0.1 |
Address to bind to |
--version, -v |
— | Print <version> (commit <short-sha>, built <yyyy-mm-dd>) and exit |
- Serves any local directory over HTTP as a read-only browser
- Embedded React SPA: markdown rendering (GFM), syntax-highlighted text viewer for 35+ languages (Go, TypeScript, Python, Kotlin, Swift, Rust, JSON, TOML, and more), inline images, download button for everything else
- Mobile-first responsive layout; desktop shows a persistent tree sidebar
- Deep-linkable URLs for every folder and file
- Light / dark / system theme toggle
- Single static binary — no runtime dependencies
- Git browsing when the served root is a git repository: commit history, branch/tag picker, per-commit diffs (lazy hunk loading), working-tree status, per-file history, and line-by-line blame
When explorer is pointed at a directory whose top level contains a .git entry (either a directory or a worktree pointer file), it exposes a small read-only git surface alongside the file browser:
- Branch picker in the toolbar (desktop) or drawer header (mobile) — switch between branches and tags, jump to local changes.
- Commits list at
/git/commits(or/git/commits?ref=<branch-or-tag>) — paginated by cursor, newest first. - Commit detail at
/git/commits/<sha>— header, parents, file list (paginated 50 at a time), per-file diffs lazily loaded in 3-hunk batches. - Working-tree status at
/git/status— staged / unstaged / untracked files; first three diffs auto-expand. - Per-file last-commit strip above every file viewer when the served root is a repo —
short_sha · subject · author · time, with aModified locallypill when the file is dirty, anUntrackedpill when it isn't tracked, and aHistorylink to the path-filtered commits list. - Code | Blame tabs on text files — switch the file view to
?view=blameto see per-line authorship grouped into commit chunks. Locally-modified lines are marked asWorking tree · uncommittedand styled distinctly so the dirty overlay is unambiguous. - Path-filtered commits at
/git/commits?path=<file>— sticky banner naming the filter with a[× clear]affordance; the sameModified locally/Untrackedpill from the file viewer appears as a status row.
The capability is strictly gated on a .git entry at the served root itself — ancestor .git directories are not walked. When the gate is not satisfied, every /api/git/* endpoint returns not_a_repo and the SPA hides all git affordances, so users of non-repo directories see exactly no change.
Everything is read-only: no checkout, commit, stage, fetch, push, or any other write operation exists. The implementation uses go-git, so the binary still has no runtime dependency on the git CLI.
explorer is designed for trusted, local use:
- Read-only. No write, update, or delete endpoints exist. The binary never modifies the served tree.
- Loopback by default.
--hostdefaults to127.0.0.1, so out of the box only your own machine can reach it. - Path-traversal containment. Every path-accepting endpoint resolves the request relative to the served root, rejects
..and absolute paths, and follows symlinks only if the resolved target stays inside the root. Symlinks pointing outside the root are silently dropped from listings and return400 outside_rootwhen accessed directly. - No authentication. There is no login, no token, no per-file ACL.
Because there is no authentication, --host 0.0.0.0 exposes every file under the served root to anyone who can reach your machine on the network. Only do this on a network you trust. Do not expose explorer to the public internet.
Two-process dev workflow: Go server on :8080 + Vite dev server on :5173 proxying API/raw paths to the Go server.
make dev-server # builds and runs the Go binary against ./testdata
make dev-web # starts the Vite dev server on :5173 (run alongside dev-server)
make test # go test ./cmd/... ./internal/...
make web-commit # rebuild the embedded SPA and stage internal/server/ui/dist/
make clean # remove ./explorer and internal/server/ui/dist
# Git-reader capability
make sample-repo # (re)generate ./testdata/sample-repo with deterministic commits
make dev-git-server # run the Go binary against the sample git repo (depends on sample-repo)
make e2e # build + sample-repo + run Playwright e2e suite (chromium)
make e2e-deps # one-time: install playwright browser system libs (needs sudo on linux)The make e2e target builds the binary, regenerates the sample-repo fixture (deterministic SHAs), and runs Playwright against two instances of the binary — one pointed at the sample repo, one at a non-repo directory — to exercise the git surface end to end. On a fresh machine run make e2e-deps once to install the browser system libraries (this uses sudo). The Playwright workspace lives under e2e/ and is independent of the SPA's package.json so SPA builds don't pull Playwright as a transitive dev dep.
internal/server/ui/dist/ is committed to source control so module-proxy installs (go install, go run) ship a working SPA. Run make web-commit whenever you change anything under web/src/ so the embedded build stays in sync.
These are intentionally deferred:
- File watcher / live reload
- Search across files
Syntax highlighting for code✓ shipped — Go, TypeScript, Python, Kotlin, Swift, Rust, and 35+ more languages plus JSON/TOML/GraphQL/Protobuf; seeweb/src/components/syntax/grammars.tsfor the full list- README auto-render in folder listings
- Tree pagination per folder
- Authentication / multi-user
- Write / update / delete
- PDF, Office, video, audio preview
"connection refused" on port 8080 in Docker
Make sure the command in docker-compose.yml includes --host 0.0.0.0. The default loopback bind is not reachable through Docker's port mapping.
GET / returns a plain-text 404 (404 page not found) after a local build
The embedded SPA is empty. Run make build (not just go build) to build the frontend and embed it. If you got the binary via go install ...@latest and still see this, the published internal/server/ui/dist/ is empty — please open an issue, this should not happen.
Symlink file is missing from listing
Symlinks whose targets resolve outside the served root are silently omitted from directory listings and return a 400 outside_root error when accessed directly — this is intentional path-traversal containment.
Issues and pull requests are welcome.
- Run
make testbefore submitting a PR. - If your PR touches
web/src/, runmake web-commitand include the regeneratedinternal/server/ui/dist/in the same commit. The embedded SPA is shipped via the Go module proxy, so a staledist/meansgo installusers get an outdated frontend against an updated API.
MIT © anantadwi13

