Skip to main content

Nookwire

Nookwire gives an agent or human temporary SSH command, SFTP, and SCP access to an ephemeral workspace. It uses AsyncSSH for the server and a pluggable public ingress: srv.us by default, Upterm over WSS, a Cloudflare Worker relay, or Cloudflare Tunnel (cloudflared).

The server binds to localhost, authenticates as the host's own OS user with standard ~/.ssh/authorized_keys or a generated password fallback, starts shell commands in the configured root, and maps SFTP and modern SCP paths to mirror shell visibility (relative paths begin in the project root, absolute paths refer to host filesystem paths). File transfers can optionally be confined to the project root with --confine-sftp. Interactive clients get a real login PTY with job control, window resizing, and the account's normal shell and prompt. The srv.us and Cloudflare backends carry opaque SSH bytes end to end. Upterm terminates and re-establishes SSH at its relay, so it uses public keys or --accept, not Nookwire passwords, and requires trusting the selected Upterm relay. The printed commands disable host-key persistence for these disposable environments.

Use Nookwire only on systems and workspaces you own or are explicitly authorized to administer. It provides authenticated access with the permissions of the host OS account and is not an OS-level sandbox. Public-key authentication is the recommended default. See Security model and SECURITY.md before exposing a workspace.

Prerequisites

The remote machine needs Python 3 and uv, and nothing else for the srvus or upterm backends. The cloudflare backend needs a deployed Worker; the cloudflared backend needs the cloudflared binary. A connecting machine's requirements depend on the backend (see Backends): srvus needs OpenSSH and OpenSSL; upterm and cloudflare need OpenSSH and Nookwire installed for their WSS ProxyCommand; cloudflared needs OpenSSH and cloudflared.

Install

PyPI

Install Nookwire with uv:

uv tool install nookwire

GitHub installer

To install the current GitHub version directly:

curl -fsSL https://raw.githubusercontent.com/lars-hagen/nookwire/main/install.sh | sh

Run it on the remote machine you want to expose. It installs nookwire as a uv tool from the Git repository into ~/.local/bin; add that directory to PATH if needed. If uv is missing, the installer fetches it from https://astral.sh/uv first; if python3 is missing, it provisions a managed Python through uv.

Once installed, nookwire upgrade re-runs the installer in place (nookwire upgrade REF pins a branch or tag; default main). Background processes are launched as sys.executable -m nookwire_ssh.server / nookwire_ssh.tunnel, so uv is never needed at runtime. Restart a running server with nookwire restart to pick up new code.

Any arguments after -- are passed to nookwire, so a single command can install and start in one go. Exposing the current directory:

curl -fsSL https://raw.githubusercontent.com/lars-hagen/nookwire/main/install.sh \
  | sh -s -- start

Or with an explicit directory, port, and srv.us slot:

curl -fsSL https://raw.githubusercontent.com/lars-hagen/nookwire/main/install.sh \
  | sh -s -- start . 8022 1

Upgrading from the old command

Existing installations can run nookwire-ssh upgrade once to move to Nookwire. The old command remains temporarily as a deprecated bridge and prints a warning directing users to nookwire. Existing state and environment settings continue to work.

Start

Start AsyncSSH and the tunnel together in the background. The default backend is srv.us; see Backends for the Cloudflare options.

nookwire start

DIR defaults to the current directory and accepts relative paths, so start alone exposes the directory you are in. Pass a path to expose somewhere else, and set the port or srv.us slot with flags or positionally:

nookwire start /marimo
nookwire start . --port 8022 --slot 1
nookwire start /marimo 8022 1

The command prints the generated password, srv.us URL, and a ready-to-run TLS-wrapped SSH command. It returns to the shell while both services keep running. status prints the same connection details later.

Repeated start is idempotent when both the server and tunnel are already running: it returns success and displays the current status rather than failing. This handles ASGI hot reloads cleanly. If only one process is running, start reports an error to prevent inconsistent partial state.

Noninteractive and batch mode: --batch

In automated scripts, CI pipelines, or background container entrypoints, use --batch or NOOKWIRE_BATCH=1:

nookwire start --batch

Nookwire batch mode never opens /dev/tty or prompts for an authorized key. It uses the existing generated password path unless --accept or key-only backend rules apply.

Important distinction: Nookwire --batch controls host-side interactive prompts and TTY access. It is distinct from OpenSSH client BatchMode (-o BatchMode=yes), which disables password and passphrase querying on the connecting client machine.

Wide-open share mode: --accept

--accept skips authentication entirely: any connecting client is admitted without a key or password, no random password is generated or stored, and the start/status output replaces the credentials with an explicit warning. Combine it with the tcp forwarding flag below for an instant share:

nookwire start --accept

Only use --accept in scenarios where wide-open access is intended (for example a short-lived throwaway workspace). Do not expose it publicly on a system you do not own.

TCP forwarding: --allow-tcp-forwarding

By default, a connected client cannot tunnel through the session. With --allow-tcp-forwarding, clients can use SSH local forwarding (ssh -L) to reach TCP destinations visible to the host, mirroring upterm's --allow-local-tcp-forwarding:

nookwire start --allow-tcp-forwarding
# on the connecting machine:
ssh -L 8080:db.internal:5432 USER@HOST ... # reaches db.internal:5432 from the host's network

The AsyncSSH server enables direct-tcpip channels only when this flag is set; otherwise such requests are refused.

Inspecting and stopping

nookwire status
nookwire connect
nookwire logs
nookwire logs tunnel -f
nookwire stop
nookwire restart

A successful start saves its settings in the state directory. Any options omitted on a later start fall back to the saved values. --accept and --allow-tcp-forwarding are never restored automatically; safety flags must be provided explicitly every time.

Deterministic Project Identity

srv.us derives the assigned public endpoint from the client's tunnel key and slot number. When running in ephemeral containers where the filesystem is reset on reboot, Nookwire automatically establishes a deterministic project identity.

Identity Hierarchy

When no existing Nookwire identity key exists, Nookwire determines tunnel identity using this strict hierarchy:

  1. Explicit secret seed (NOOKWIRE_IDENTITY_SEED, alias NOOKWIRE_SSH_IDENTITY_SEED): Uses the established deterministic derivation and keeps byte-for-byte compatibility with existing seeds.
  2. Explicit non-secret identity (NOOKWIRE_IDENTITY, alias NOOKWIRE_SSH_IDENTITY): Derives identity from ${username}@${NOOKWIRE_IDENTITY}.
  3. Normalized Git origin: Detects the Git remote origin of the configured root directory and normalizes equivalent SSH and HTTPS remotes (e.g. git@github.com:owner/repo.git and https://github.com/owner/repo) to github.com/owner/repo. Combined as ${username}@${normalized_origin}.
  4. CI/PaaS project variable: Inspects a strict allowlist of project identifiers: GITHUB_REPOSITORY, CI_PROJECT_PATH, BITBUCKET_REPO_FULL_NAME, CIRCLE_PROJECT_REPONAME, RENDER_SERVICE_NAME, RAILWAY_PROJECT_NAME, VERCEL_GIT_REPO_SLUG. Combined as ${username}@${project_id}.
  5. Host-local identity: Derived from ${username}@${host_id}:${resolved_root} using /etc/machine-id, Windows MachineGuid, or platform.node().
  6. Secure random fallback: Generates a cryptographically secure random key if no stable selector can be formed. Volatile attributes (container/pod IDs, MAC addresses, IPs, inodes, Git commit SHAs) are never used.

Security and Endpoint Continuity

Automatic identity is explicitly non-secret and provides endpoint continuity, not ownership secrecy. Anyone with knowledge of the repository URL or project name in the same environment can derive the same tunnel identity. To prevent collision or unauthorized endpoint reuse, operators requiring exclusive ownership must provide a secret NOOKWIRE_IDENTITY_SEED.

The srv.us slot acts as routing input, allowing one persistent tunnel key across multiple slots.

Identity Command

Inspect identity details:

nookwire identity
nookwire identity --json

Outputs mode (seeded, project, host, random), source, selector fingerprint, and key path/fingerprint without exposing secret seeds or raw selectors.

Automation and JSON Output

Nookwire provides machine-readable outputs for agent and CI integration:

Machine-readable status

nookwire status --json

Emits stable JSON to stdout without ANSI codes:

{
  "version": "2.5.0",
  "backend": "srvus",
  "server_state": "running",
  "server_pid": 12345,
  "tunnel_state": "running",
  "tunnel_pid": 12346,
  "url": "https://a1b2c3d4.srv.us/",
  "host": "a1b2c3d4.srv.us",
  "ssh_username": "appuser",
  "auth_mode": "password",
  "forwarding": false,
  "sftp_mode": "host",
  "sftpMode": "host",
  "sftp_root": "/marimo",
  "sftpRoot": "/marimo",
  "identity_mode": "project",
  "identity_source": "git:origin",
  "identity_fingerprint": "8d3e91a0c4f2",
  "connect_command": "ssh appuser@a1b2c3d4.srv.us -o 'ProxyCommand=...' ..."
}

Batch connect command

SSH_CMD=$(nookwire connect --batch)
eval "$SSH_CMD" uptime

connect --batch prints exactly one self-contained SSH command line with -T -o BatchMode=yes and required proxy options, with no prose or trailing commentary. connect --json is also available.

Backends

start --backend selects the public ingress. The AsyncSSH server is identical across all backends; only the tunnel process and the printed connect command change. status reports the right command for whichever backend is running.

srvus (default)

Reverse tunnel over srv.us. Zero account, zero domain; the connecting machine needs only OpenSSH and OpenSSL. See Connect through TLS.

nookwire start --backend srvus --slot 1

cloudflare (Worker relay)

Relays SSH over WebSockets via a Cloudflare Worker and Durable Object. Deploy the Worker in worker/:

cd worker && npx wrangler deploy

Then start with the deployed URL as --endpoint:

nookwire start --backend cloudflare \
  --endpoint https://nookwire.<subdomain>.workers.dev

upterm (public WSS relay)

Uses Upterm's public relay over outbound WSS on port 443 and keeps Nookwire's local AsyncSSH server, PTY, SFTP/SCP configuration, and forwarding policy:

nookwire start --backend upterm

The backend requires at least one key in ~/.ssh/authorized_keys, unless --accept is explicitly supplied.

cloudflared (Cloudflare Tunnel)

Uses cloudflared with a named tunnel:

nookwire start --backend cloudflared \
  --hostname ssh.example.com --token "$CF_TUNNEL_TOKEN"

Shorten the connect command

Add the proxy command to ~/.ssh/config once on the connecting machine:

nookwire ssh-config --write

Afterwards:

ssh USER@HOSTNAME.srv.us
sftp USER@HOSTNAME.srv.us
scp notebook.py USER@HOSTNAME.srv.us:notebook.py

Connect through TLS

The srvus backend wraps non-HTTP traffic in TLS. start and status print the ready-to-use SSH form:

ssh USER@HOSTNAME.srv.us \
  -o 'ProxyCommand=openssl s_client -quiet -verify_return_error -verify_hostname %h -connect %h:443 -servername %h 2>/dev/null' \
  -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR

Security model

  • Public-key authentication is recommended and automatically uses ~/.ssh/authorized_keys; password authentication remains available as a temporary fallback and uses constant-time comparison.
  • --accept disables both, admitting any client without credentials. Only use it where open access is intended.
  • TCP port forwarding is disabled by default; --allow-tcp-forwarding enables ssh -L through the session.
  • The generated password is removed from command environments.
  • By default, SFTP and modern SCP mirror shell visibility: relative paths begin in the configured project root, while absolute paths refer to host filesystem paths subject to OS user permissions.
  • --confine-sftp (or NOOKWIRE_CONFINE_SFTP=1 / NOOKWIRE_SSH_CONFINE_SFTP=1) confines SFTP and SCP transfers to the project root virtual namespace and rejects symlink escapes. This limits file-transfer clients to the project directory, but because Nookwire executes shell commands as the host OS user, it does not sandbox shell or exec commands.
  • Command sessions start in the root but are not OS-chrooted. Authenticated users can access anything allowed to the server's operating-system account.
  • The server generates and reuses an Ed25519 host key in a private per-user directory.
  • The connection examples disable host-key persistence because this targets short-lived disposable environments.
  • Automatic project identity is non-secret and provides endpoint continuity, not ownership secrecy. Use NOOKWIRE_IDENTITY_SEED for exclusive endpoint control.

Release and Publishing

Release publication is automated via GitHub Actions using PyPI Trusted Publishing (.github/workflows/publish.yml).

The workflow triggers on GitHub Release published, builds the sdist and wheel distributions, and publishes them to PyPI using least-privilege OIDC tokens (contents: read, id-token: write). Tests and installation checks run locally before release so the publishing job stays fast.

To configure the PyPI pending publisher:

  • PyPI project name: nookwire
  • Owner: lars-hagen
  • Repository name: nookwire
  • Workflow name: publish.yml
  • Environment name: pypi

Development

uv sync
uv run python -W error::ResourceWarning -m unittest discover -s tests -v
uv run python -m py_compile src/nookwire_ssh/*.py tests/*.py
sh -n install.sh

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nookwire-2.5.0.tar.gz (61.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nookwire-2.5.0-py3-none-any.whl (46.7 kB view details)

Uploaded Python 3

File details

Details for the file nookwire-2.5.0.tar.gz.

File metadata

  • Download URL: nookwire-2.5.0.tar.gz
  • Upload date:
  • Size: 61.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for nookwire-2.5.0.tar.gz
Algorithm Hash digest
SHA256 cc037d78a58758e9519881ccdaa4400e84009deb966aab74a5637453bfb37b6d
MD5 856446dc434325f4ae6dbe0cf41be55a
BLAKE2b-256 8e8cfb5f98a0bd3575d714fc87c0de0246aefe8f4f3ff4dae8eb6e5fa5e5ba76

See more details on using hashes here.

File details

Details for the file nookwire-2.5.0-py3-none-any.whl.

File metadata

  • Download URL: nookwire-2.5.0-py3-none-any.whl
  • Upload date:
  • Size: 46.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for nookwire-2.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 63ff73852c99cee703f7e1dfb0fa7d3d3e983fd29aae7276b8e8d9c0e975eab2
MD5 5664de54a8a62087d206cb03c24f8b11
BLAKE2b-256 ed9840b0da5b7a5b5621239d41713e22aa493c1f1dc7e5e1bec7f320f3874adb

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

2.5.0 This release

2 files

2.4.1

2 files

2.4.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page