A single-binary file browser for local and SSH/SFTP paths, with JupyterLab, TensorBoard, and LaTeX support.
open-server is designed to run for one user on a personal device. It provides
browser-based access to files on that device or a remote SSH host. Remote access
uses the system ssh client and the standard SFTP subsystem, so existing
aliases, keys, agents, host-key checks, ProxyJump, and other ssh_config
settings remain in use. The open-server binary remains on the personal device
and is not installed on the remote machine.
- Browse local paths and remote SSH/SFTP paths without installing
open-serveron the remote host. - Create directories; upload, paste, import, preview, and download files; copy paths; sort and filter directory contents; and show or hide hidden entries.
- Open several local paths, remote paths, or saved sessions in one command.
- Launch TensorBoard for event-log directories and JupyterLab for project directories, including remote processes reached through SSH tunnels.
- Generate LaTeX table and figure snippets and follow rebuilt PDF files.
- Share a local path from the personal device over a trusted network.
open-server is a file browser, not a web editor or file-synchronization
service.
Prebuilt Linux and macOS binaries for amd64 and arm64 are available from GitHub Releases. Building from source requires Go 1.25.12 or newer. Remote browsing also requires a local OpenSSH-compatible client and an account whose server enables SFTP. Optional components such as TensorBoard or JupyterLab must be installed on the machine containing the files.
# Build from source.
go build -o open-server ./cmd/open-server
# Browse a local project.
./open-server ./project
# Browse a remote project through SSH/SFTP.
./open-server lab:~/projects
# Enable the research functions for a remote project.
./open-server lab:/data/project -latex -tensorboard -jupyterThe program prints a loopback URL and normally opens it in the default browser.
Use -no-open to print the URL without opening a tab.
Use Close open-server in any local or SSH/SFTP file-browser header to exit
the program and stop all of its sessions, including launched JupyterLab or
TensorBoard processes. The result page records the user-close time and advises
using Ctrl-C in the terminal if manual shutdown is needed. The control is not
available with -serve.
Remote targets use host:path syntax:
lab:/data/project absolute remote path
lab:projects path relative to the SFTP working directory
lab:~/projects path relative to the SFTP working directory
Local directories and files open directly. Use -local when a bare name could
be mistaken for a saved session.
./open-server .
./open-server -local README.md
./open-server work lab:/data/results ./local-paperFrequently used targets can be saved under a name:
./open-server --add work lab:~/projects
./open-server --list
./open-server work
./open-server --delete work
./open-server --editOptions supplied to --add are saved with the target. Explicit command-line
options override saved values. Run ./open-server -help for the complete CLI
reference.
Saved sessions keep their configured port. If that port is already in use,
open-server reports that another session may be running instead of silently
changing the saved port. Use --allow-port-change to choose and remember
another available port, or run open-server --edit to assign a specific port.
The file browser uses a 14 px base font. Choose an integer size from 8 through
72 with -fontsize; controls and dialogs scale with it, while compact table
padding gives the text more of each row's height. The option can also be saved
with a named session:
./open-server lab:~/projects -fontsize 18
./open-server --add work lab:~/projects -fontsize 18SSH/SFTP sessions reconnect automatically after an unexpected drop, using
bounded retries with backoff. The local URL and port stay the same, so open
browser tabs do not need to be reloaded: file operations return a temporary
503 while the SSH connection is down, and normal browsing resumes once it
reconnects. Use -no-reconnect to disable this and end the session
immediately on a drop instead.
Remote TensorBoard and JupyterLab processes are not stopped when the SSH
connection drops; they keep running so a brief interruption does not lose
in-progress work, and open-server re-attaches their tunnels automatically
once reconnected. -reconnect-grace (default 30m) bounds how long they wait
for that reconnection before shutting down. Closing the session explicitly,
whether from Close open-server or Ctrl-C, still stops them immediately
regardless of the grace window.
Directories can be sorted by name, modification time, or size, and hidden files
can be shown when needed. open-server can create directories, upload multiple
files, save a file from the clipboard, import a file from a URL, copy full
paths, and download original files. Existing destination files are not replaced
without an explicit overwrite choice.
The Jump to box below the header line goes straight to a folder inside the served root. A relative path is resolved against the root; an absolute path is accepted when it stays inside it, including symlink-equivalent spellings such as the physical location of a symlinked root (or a symlink alias of a physical one), which are mapped back onto the configured root so the displayed root path never changes. Only existing folders are accepted, problems are reported inline, and the current sort order and hidden-file state carry over.
Directory listings can be filtered in the browser with a regular expression matched against each entry's basename. Matching is case-sensitive by default; enable Ignore case when needed. While a filter is applied, an Active badge and blue highlighting on the input and match count make the filtered state obvious. The current valid filter is remembered separately for each folder for the life of the browser tab. Use the ? button beside the filter for syntax notes and examples.
The compact Actions group has fixed open-in-new-tab, copy-path, and file-download subcolumns, so each action stays vertically aligned even when another action is unavailable. The table uses only the width its columns need instead of stretching across a full-screen window. Permanently visible separators after Name, Last modified, and Size show that those columns are adjustable. Drag a separator to resize its column; the browser remembers the widths across directories. The separators also support Left/Right Arrow (Shift for a larger step), Home to restore the default, and double-click to reset. Truncated values retain their full text as a hover tooltip.
URL imports are fetched by the personal device running open-server, including
in SSH/SFTP and sharing modes.
Preview and download are separate operations. Preview uses a safe representation when possible, while download always returns the original file as an attachment.
- HTML, HTM, and XHTML render as static documents. Relative CSS, images, fonts, and media can load from the same browsed directory; scripts, forms, frames, objects, workers, and external network connections remain disabled. Root-relative site paths are not remapped into the browsed directory.
- Markdown (
.mdand.markdown) is rendered on the server with tables, task lists, strikethrough, autolinks, and heading IDs. Relative images and links use the corresponding preview routes. Raw HTML and dangerous link schemes are omitted. Syntax highlighting and math rendering are not included. - SVG opens through an
open-serverpage as an image rather than as an interactive document. Scripts, event handlers, and external resources are disabled; self-contained declarative animation may still run. - Plain text, source code, CSV/TSV, XML, JavaScript, CSS, EPS/PostScript, TeX,
PGF, and TikZ otherwise remain inert source previews. They are not compiled
or executed by
open-server. - Raster images, supported audio/video, and PDFs use the browser's native viewer. Unknown binary formats fall back to download.
- Preview responses are sandboxed, prevent MIME sniffing and framing, and are restricted to the current browser origin.
Directory listing does not open or parse preview files. HTML and SVG content is streamed when selected; Markdown is read and parsed only when selected. There is no preview-size limit, so unusually large Markdown files or pathological HTML/SVG graphics can still consume browser or server CPU and memory.
PDF previews use Chrome's PDFium or Firefox's PDF.js inside the browser sandbox. The live viewer uses the bundled PDF.js runtime with embedded PDF scripting disabled. The server does not execute PDF content or invoke an external renderer.
LaTeX actions are enabled automatically for local targets. Add -latex for
SSH/SFTP or sharing sessions.
./open-server lab:/data/paper -latex| File | Function |
|---|---|
.csv, .tsv |
Copy a table environment or its inner \csvautotabular command. |
.png, .jpg, .jpeg, .pdf |
Copy a figure environment or its inner \includegraphics command. |
.pdf |
Follow a compiled PDF, waiting for a completed and stable file before reloading. |
Use the Short / Full env switch below the LaTeX heading to choose the copied snippet format. Full environments remain the default, and the browser remembers the selected format while navigating between directories.
The live viewer keeps the currently displayed page across rebuilds. If a rebuilt PDF has fewer pages, it clamps to the last available page (or page 1 before a document is available), and that clamped page remains selected on later rebuilds.
The generated snippets retain the full path from the active filesystem, so a
TeX build on the remote machine can use the listed artifact in place. Figure
snippets require graphicx; table snippets require csvsimple.
./open-server lab:/data/runs -tensorboard
./open-server lab:/data/runs -tensorboard -py /opt/venv/bin/pythonTensorBoard actions are shown for every directory without scanning its
contents. When an action is selected, open-server checks that the directory
directly contains an events.out.tfevents.* file before starting TensorBoard.
In SSH/SFTP mode, it starts TensorBoard on the remote host through SSH, creates
the tunnel, and proxies it through the current session. This does not install
open-server on the remote host. Repeated launches reuse the running process
for that directory. Its two-column control changes from Start to Open
when ready and reveals Stop, so it can be reopened or stopped without
ending open-server. TensorBoard actions are unavailable with -serve.
Remote TensorBoard listens on a per-launch Unix socket inside a user-owned
0700 runtime directory. The socket is forwarded through the same SSH session
that owns the process and also requires a random token injected by the local
proxy. The remote helper disables TensorBoard's secondary fast-loader and gRPC
data-provider listeners. If Unix-socket forwarding is unavailable, the launch
fails rather than falling back to remote TCP. The selected Python environment
and its installed TensorBoard plugins are trusted executable code. Local
TensorBoard has no equivalent boundary: it is stock tensorboard on loopback
with no token, which assumes a personal device. See Security and scope.
By default, tensorboard must be on PATH on the machine containing the files.
Use -py or --python-interpreter when it belongs to a virtual or Conda
environment.
./open-server lab:/data/project -jupyter
./open-server lab:/data/project -jupyter -py /opt/venv/bin/pythonJupyterLab can be started for any directory in the session. The Python kernel
executable can be selected at launch. The -py or --python-interpreter value
is the default and also supplies the JupyterLab installation; another selected
environment only needs ipykernel. Other kernels already installed for Jupyter
remain available from JupyterLab's kernel menu.
JupyterLab is proxied through the current browser URL. In SSH/SFTP mode,
open-server launches it on the remote host through SSH and creates the tunnel
without installing open-server remotely or exposing the Jupyter token in the
browser. One JupyterLab instance may run for each directory. Repeated starts
reuse that instance; its selected Python executable remains fixed until it is
stopped. The two-column control changes from Start to Open when ready
and reveals Stop. Stopping asks for confirmation because it terminates all
kernels owned by that JupyterLab instance; after it stops, another Python
executable can be selected for the next start.
Files deleted from JupyterLab are moved to a persistent trash directory inside
the directory that was launched: .Trash-<uid>/files/. Matching recovery
metadata is stored in .Trash-<uid>/info/. Keeping trash on the working
filesystem avoids send2trash failures when the user cannot create a trash
directory at a mount point. The trash is hidden by default and is not removed
when the open-server session ends.
Remote JupyterLab uses the same private Unix-socket and proxy-injected-token boundary. Kernel connection files and IPC endpoints stay inside the private runtime directory; unsupported Jupyter versions fail the launch rather than falling back to TCP. The selected Jupyter environment, extensions, kernels, and kernel provisioners are trusted executable code.
When the open-server session ends, it asks Jupyter to stop every kernel, then
terminates the complete Jupyter process group and removes the temporary kernel
registration. open-server does not disable JupyterLab's built-in notebook
autosave; the interval configured in the selected JupyterLab environment still
applies. JupyterLab actions are unavailable with -serve.
The new table layout and column resizing are browser-only and add no server processes, dependencies, or background work. With both application flags disabled, there are no application controls or status requests. Enabling a flag but leaving every application stopped also launches no child process and generates no status polling. Reloading a directory obtains its current state while rendering the normal listing.
Each running application adds one small open-server lifecycle record and one
waiting Go goroutine. A visible running control polls its in-memory status once
every 10 seconds (0.1 request/second); polling pauses while the tab is hidden.
These controller costs are normally negligible compared with the launched
Python application. Each directory started in TensorBoard or JupyterLab has its
own Python service; Jupyter kernels add their own processes, and TensorBoard's
memory use grows with event data and plugins. In SSH/SFTP mode, each application
also has an SSH tunnel and a lightweight remote keeper during the reconnect
grace period. Capacity should therefore be planned from the actual Jupyter
kernels, TensorBoard logs, and number of concurrently running directories, not
from the controls themselves.
-serve shares a local path from the personal device with another device on a
trusted network:
./open-server -serve /data/project
./open-server -serve -address 10.0.0.5 -port 60123 /data/projectWith no path, -serve uses the current directory. By default, it binds all IPv4
interfaces, chooses an available port from 60000, and generates a random access
token. The initial token URL is exchanged for a token-scoped HTTP-only cookie
and then removed from the address bar. TensorBoard and JupyterLab actions are
not available in this mode.
Warning
Sharing mode uses token-protected but unencrypted HTTP. The token limits access; it does not encrypt URLs, file names, uploads, downloads, or cookies. Use this mode only on a trusted network or private VPN. Prefer the default loopback SSH/SFTP mode for sensitive data.
open-server assumes a single trusted OS user on the personal device. In
ordinary local and SSH/SFTP modes, the web listener is restricted to IPv4
loopback. It validates the exact Host header and requires the exact local
Origin for state-changing requests. Static HTML uses a sandboxed browser MIME
type with scripts and active capabilities disabled. SVG is loaded only in image
mode with a stricter resource policy, and Markdown is converted with raw HTML
and dangerous links disabled. JavaScript and other source formats remain inert
plain text.
Local -tensorboard and -jupyter inherit that assumption. Their outer
open-server proxy listens on IPv4 loopback without a token, and loopback is
not a per-user boundary: on a multi-user host, every other account can reach
the enabled services through that proxy. Local JupyterLab still requires its
own random token, but open-server supplies it to proxied requests; local
TensorBoard has no child-service token. The SSH/SFTP form is the supported way
to use these against a shared server, because each service is then confined to
a 0600 Unix socket in a user-owned 0700 runtime directory and additionally
requires a proxy-injected token. Uploads through SSH/SFTP are staged privately
and published with mode 0600 for the same reason.
The starting path is a navigation boundary, not a filesystem sandbox. Parent navigation stops there, but symlinks may lead outside it. Filesystem permissions of the local or SSH account remain the final boundary.
go test ./...
go test -race ./...
go vet ./...