From e09679d753db73a2c3ac823424acbec858c3a48f Mon Sep 17 00:00:00 2001 From: Tyler Vick Date: Wed, 29 Jul 2026 11:47:30 -0700 Subject: [PATCH 1/2] docs: day-to-day Claude Code setup, as validated on hardware Replaces the generic SSH-tunnel section with the exact flow proven end-to-end against a real device: master socket carrying the tunnel (one auth), the dropbear agent-flood gotcha, tunnel liveness check, token-to-file with by-reference usage everywhere, and fresh-session pickup. Durable access is #16; other MCP clients are #17. Closes #9. Co-Authored-By: Claude Fable 5 --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8472aa0..5f9b413 100644 --- a/README.md +++ b/README.md @@ -182,34 +182,65 @@ tunnel/port-forward — see [Security](#security-model) for why the LAN address discouraged) and `` with the value of `NANOKVM_MCP_TOKEN`, or the token printed to `/data/nanokvm-mcp/daemon.log` if you didn't set one. -### Over an SSH tunnel +### Claude Code over an SSH tunnel (tested end-to-end) -With the default loopback bind, forward the port from your machine (the keepalive matters -— an idle MCP session will otherwise drop the tunnel): +This is the setup validated against real hardware. It uses an SSH master +connection that carries the port-forward and authenticates once; every later +`ssh`/`scp` rides the same socket without re-prompting. (The hourly lapse makes +this a per-session flow, not a permanent one — a durable path is tracked in +[#16](https://github.com/tylervick/nanokvm-mcp/issues/16).) + +**1. Open the master connection + tunnel** (one password prompt): ```sh -ssh -N -L 8080:127.0.0.1:8080 -o ServerAliveInterval=30 root@ +ssh -f -N -M -S /tmp/nkvm.sock -o ControlPersist=3600 -o ServerAliveInterval=30 \ + -o PubkeyAuthentication=no -o PreferredAuthentications=password -o IdentitiesOnly=yes \ + -L 8080:127.0.0.1:8080 root@ ``` -Then the endpoint is `http://127.0.0.1:8080/` locally. Keep the token out of your -shell history: export it once (`export NANOKVM_MCP_TOKEN=...` with a leading space, -or `export NANOKVM_MCP_TOKEN=$(cat token-file)`) and reference the variable so the -literal value never appears on a command line. Verify with MCP Inspector: +The `PubkeyAuthentication=no` trio matters: the stock firmware has no authorized +keys, and an ssh-agent holding several keys will exhaust dropbear's auth attempts +before password auth is ever offered ("Too many authentication failures"). + +Check whether the tunnel is still alive later (`ControlPersist=3600` = one hour): ```sh -npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8080/ \ - --transport http --header "Authorization: Bearer ${NANOKVM_MCP_TOKEN}" --method tools/list +ssh -S /tmp/nkvm.sock -O check root@ # "Master running" = alive +``` + +**2. Pull the bearer token to a local file** — by reference from here on, so the +secret never enters shell history or configs: + +```sh +ssh -S /tmp/nkvm.sock root@ \ + 'sed -n "s/^NANOKVM_MCP_TOKEN=//p" /root/nanokvm-mcp/nanokvm-mcp.env' \ + > ~/.nanokvm-token && chmod 600 ~/.nanokvm-token ``` -Or add it to Claude Code — single quotes matter: they pass the `${VAR}` reference -through unexpanded, and Claude Code's `.mcp.json` expands it at connection time, -so neither your shell history nor the saved config ever contains the secret: +**3. Register with Claude Code** — the single quotes are load-bearing: they pass +the `${VAR}` reference through unexpanded, and Claude Code's `.mcp.json` expands +it at connection time, so the saved config holds the reference, not the secret: ```sh +export NANOKVM_MCP_TOKEN=$(cat ~/.nanokvm-token) # add to your shell profile claude mcp add --transport http nanokvm http://127.0.0.1:8080/ \ --header 'Authorization: Bearer ${NANOKVM_MCP_TOKEN}' ``` +**4. Use it.** Start a *fresh* Claude Code session (new MCP servers are picked up +at session start) from a shell where the variable is set, and ask for a +screenshot or device info. In the default read-only mode the 7 read-only tools +appear; mutating tools require `NANOKVM_MCP_READONLY=false` in the device env +plus an init-script restart, after which they show up annotated as destructive — +Claude asks before using them, and every call lands in the audit log. + +To sanity-check the endpoint without Claude, use MCP Inspector: + +```sh +npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8080/ \ + --transport http --header "Authorization: Bearer ${NANOKVM_MCP_TOKEN}" --method tools/list +``` + ## Tools 14 tools are registered by default (7 in read-only mode): From 5f6613c7da4666ebccabc654947db29756f8184a Mon Sep 17 00:00:00 2001 From: Tyler Vick Date: Wed, 29 Jul 2026 11:55:47 -0700 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20address=20review=20=E2=80=94=20Exit?= =?UTF-8?q?OnForwardFailure,=20idle-timeout=20wording,=20token-file=20hard?= =?UTF-8?q?ening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ControlPersist=3600 described as an idle timeout, not a 1-hour cap - ExitOnForwardFailure=yes so a failed -L bind fails loudly instead of backgrounding a broken master - token pull: umask 077 subshell (no permissive window) + test -s guard for the unset-token path - note that the Inspector CLI example exposes the token to ps (no file-based header exists in Inspector); curl @file suggested for shared hosts Co-Authored-By: Claude Fable 5 --- README.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5f9b413..99fe09d 100644 --- a/README.md +++ b/README.md @@ -186,23 +186,28 @@ discouraged) and `` with the value of `NANOKVM_MCP_TOKEN`, or the token p This is the setup validated against real hardware. It uses an SSH master connection that carries the port-forward and authenticates once; every later -`ssh`/`scp` rides the same socket without re-prompting. (The hourly lapse makes -this a per-session flow, not a permanent one — a durable path is tracked in -[#16](https://github.com/tylervick/nanokvm-mcp/issues/16).) +`ssh`/`scp` rides the same socket without re-prompting. (The master exits after +an hour *idle*, so this is a per-session flow, not a permanent one — a durable +path is tracked in [#16](https://github.com/tylervick/nanokvm-mcp/issues/16).) **1. Open the master connection + tunnel** (one password prompt): ```sh ssh -f -N -M -S /tmp/nkvm.sock -o ControlPersist=3600 -o ServerAliveInterval=30 \ + -o ExitOnForwardFailure=yes \ -o PubkeyAuthentication=no -o PreferredAuthentications=password -o IdentitiesOnly=yes \ -L 8080:127.0.0.1:8080 root@ ``` +`ExitOnForwardFailure=yes` makes a failed port-forward (e.g. 8080 already taken +by a stale tunnel) fail loudly here instead of backgrounding a broken master. + The `PubkeyAuthentication=no` trio matters: the stock firmware has no authorized keys, and an ssh-agent holding several keys will exhaust dropbear's auth attempts before password auth is ever offered ("Too many authentication failures"). -Check whether the tunnel is still alive later (`ControlPersist=3600` = one hour): +Check whether the tunnel is still alive later (`ControlPersist=3600` means the +master exits after 3600 s with no client connections — active use keeps it open): ```sh ssh -S /tmp/nkvm.sock -O check root@ # "Master running" = alive @@ -212,11 +217,15 @@ ssh -S /tmp/nkvm.sock -O check root@ # "Master running" = alive secret never enters shell history or configs: ```sh -ssh -S /tmp/nkvm.sock root@ \ +(umask 077; ssh -S /tmp/nkvm.sock root@ \ 'sed -n "s/^NANOKVM_MCP_TOKEN=//p" /root/nanokvm-mcp/nanokvm-mcp.env' \ - > ~/.nanokvm-token && chmod 600 ~/.nanokvm-token + > ~/.nanokvm-token) && test -s ~/.nanokvm-token || echo "no token in env file" ``` +The `test -s` guard catches the case where no explicit `NANOKVM_MCP_TOKEN` is +set on the device (the generated-token path prints to `daemon.log` instead — +but set an explicit one; see the [first-run checklist](#first-run-checklist-learned-on-real-hardware)). + **3. Register with Claude Code** — the single quotes are load-bearing: they pass the `${VAR}` reference through unexpanded, and Claude Code's `.mcp.json` expands it at connection time, so the saved config holds the reference, not the secret: @@ -241,6 +250,11 @@ npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8080/ \ --transport http --header "Authorization: Bearer ${NANOKVM_MCP_TOKEN}" --method tools/list ``` +Note the shell expands the token into the process's arguments here (Inspector +has no file-based header option), so it's briefly visible to `ps` — fine on a +single-user machine, but on a shared host prefer checking with curl's `@file` +header form instead. + ## Tools 14 tools are registered by default (7 in read-only mode):