Skip to content

Instantly share code, notes, and snippets.

@michaelneale
Created July 18, 2026 05:04
Show Gist options
  • Select an option

  • Save michaelneale/e3cf8c01da25138017637ed184b92a25 to your computer and use it in GitHub Desktop.

Select an option

Save michaelneale/e3cf8c01da25138017637ed184b92a25 to your computer and use it in GitHub Desktop.
goose mobile tunnel — stopgap to restore mobile app <-> desktop connection (builds v1.39.0 goosed + tunnel, runs in parallel). See goose issue #10523

goose mobile tunnel — stopgap

Restore the goose mobile app ↔ desktop connection while the long-term QUIC/P2P/ACP migration (#9556) is in progress.

Tracking issue: #10523

What this is

The mobile tunnel was removed from goose after v1.39.0 (PRs #9932 and #10224). This script builds the last release that still had a working tunnel (v1.39.0) and runs just its goosed server + tunnel, so your phone can reach your desktop again.

It is a stopgap, not a fix.

How it works

mobile app ──HTTPS──▶ cloudflare worker ──WebSocket──▶ your goosed ──HTTP──▶ 127.0.0.1 (local goose REST API)

goosed opens an outbound WebSocket to a Cloudflare relay (no inbound ports, no firewall changes). The relay gives you a public URL; the mobile app connects to it with a secret. Requests are proxied to your local goose server.

Why it's safe to run alongside your current goose

  • It runs on its own port (default 3010), so it won't clash with your desktop app or CLI.
  • It reads the same ~/.config/goose config and SQLite database — no code changes, no migrations. It's just a second reader/writer, which SQLite handles.
  • It builds into an isolated directory (~/.goose-mobile-tunnel) and never touches your working copy.

Requirements

  • git, curl, openssl
  • A Rust toolchain — or nothing extra, since the script auto-uses the repo's bundled hermit toolchain if you don't have Rust installed.
  • Optional: qrencode (brew install qrencode) to print a scannable QR in your terminal.

Usage

chmod +x goose-mobile-tunnel.sh

# Build (first run only, a few minutes), start goosed, open the tunnel:
./goose-mobile-tunnel.sh

# Force a fresh rebuild:
./goose-mobile-tunnel.sh --rebuild

# Stop the background goosed started by this script:
./goose-mobile-tunnel.sh --stop

On success it prints:

  • a Tunnel URL and Secret you can type into the mobile app, and
  • a goosechat://configure?data=... deeplink (turn it into a QR code and scan it — this is exactly what the old desktop UI encoded).

Pairing the mobile app

Either:

  1. Manual — enter the printed Tunnel URL + Secret in the app, or
  2. QR — if qrencode is installed the script prints a scannable QR; otherwise paste the goosechat://configure?... deeplink into any QR generator and scan it.

Troubleshooting

  • Watch the logs: tail -f ~/.goose-mobile-tunnel/goosed.log A successful tunnel logs ✓ Connected as agent … and ✓ Public URL: ….
  • If the phone can't connect, confirm the URL is reachable (it requires the secret; without it you'll get an empty response — that's the tunnel rejecting you, which is expected).
  • Uninstall completely:
    ./goose-mobile-tunnel.sh --stop
    rm -rf ~/.goose-mobile-tunnel

Security notes

  • The tunnel is protected by a per-run secret. Treat the Tunnel URL + Secret (and the QR/deeplink) as a password — anyone with them can reach your goose.
  • The relay is a shared Cloudflare worker; traffic is end-to-end over HTTPS to the worker and proxied to your local machine. Only requests carrying the correct secret are forwarded to goosed.
#!/usr/bin/env bash
#
# goose-mobile-tunnel.sh
#
# Stopgap to restore the goose mobile app <-> desktop connection.
#
# The mobile tunnel was removed from goose after v1.39.0 (PR #9932 / #10224).
# This script builds the last release that still had the working tunnel
# (v1.39.0) in an isolated git worktree, runs its `goosed` server on its own
# port, and opens the Cloudflare tunnel the mobile app expects.
#
# It reads the SAME ~/.config/goose config + SQLite as your normal goose, and
# makes NO code changes, so it is safe to run in parallel with your current
# goose desktop/CLI.
#
# See: https://github.com/aaif-goose/goose/issues/10523
#
# Usage:
# ./goose-mobile-tunnel.sh # build (if needed), run, open tunnel
# ./goose-mobile-tunnel.sh --rebuild # force a fresh rebuild
# ./goose-mobile-tunnel.sh --stop # stop any running goosed started here
#
# Requirements: git, a Rust toolchain (or hermit, which the repo ships with),
# curl, and openssl. macOS and Linux.
#
set -euo pipefail
# ----- config (override via env if you like) --------------------------------
GOOSE_REPO="${GOOSE_REPO:-https://github.com/aaif-goose/goose.git}"
TUNNEL_TAG="${TUNNEL_TAG:-v1.39.0}" # last release with the working tunnel
WORKDIR="${WORKDIR:-$HOME/.goose-mobile-tunnel}"
SRC_DIR="$WORKDIR/src"
PORT="${GOOSE_PORT:-3010}" # own port; won't clash with desktop
PIDFILE="$WORKDIR/goosed.pid"
LOGFILE="$WORKDIR/goosed.log"
SECRETFILE="$WORKDIR/server-secret"
# ----- helpers --------------------------------------------------------------
say() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*"; }
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
stop_goosed() {
if [[ -f "$PIDFILE" ]]; then
local pid; pid="$(cat "$PIDFILE" 2>/dev/null || true)"
if [[ -n "${pid:-}" ]] && kill -0 "$pid" 2>/dev/null; then
say "Stopping goosed (pid $pid) and children..."
pkill -P "$pid" 2>/dev/null || true
kill "$pid" 2>/dev/null || true
fi
rm -f "$PIDFILE"
fi
# belt and suspenders: kill any goosed we built here
pkill -f "$WORKDIR/.*goosed agent" 2>/dev/null || true
}
# ----- arg handling ---------------------------------------------------------
REBUILD=false
case "${1:-}" in
--stop) stop_goosed; say "Stopped."; exit 0 ;;
--rebuild) REBUILD=true ;;
"" ) ;;
* ) die "Unknown option: $1 (use --rebuild or --stop)" ;;
esac
command -v git >/dev/null || die "git is required"
command -v curl >/dev/null || die "curl is required"
command -v openssl>/dev/null || die "openssl is required"
mkdir -p "$WORKDIR"
# ----- 1. get the source at the last-good tag -------------------------------
if [[ ! -d "$SRC_DIR/.git" ]]; then
say "Cloning goose @ $TUNNEL_TAG (shallow) into $SRC_DIR ..."
git clone --depth 1 --branch "$TUNNEL_TAG" "$GOOSE_REPO" "$SRC_DIR"
else
say "Source already present at $SRC_DIR"
fi
cd "$SRC_DIR"
# Prefer the repo's hermit toolchain if present, else fall back to system cargo.
if [[ -f bin/activate-hermit ]]; then
say "Activating hermit toolchain..."
# shellcheck disable=SC1091
source bin/activate-hermit
fi
command -v cargo >/dev/null || die "cargo (Rust) not found. Install Rust: https://rustup.rs"
# ----- 2. build goosed ------------------------------------------------------
BIN="$SRC_DIR/target/release/goosed"
if [[ "$REBUILD" == true || ! -x "$BIN" ]]; then
say "Building goosed (release). First build can take several minutes..."
cargo build --release -p goose-server --bin goosed
else
say "Reusing existing build: $BIN"
fi
[[ -x "$BIN" ]] || die "Build did not produce $BIN"
# ----- 3. run goosed on its own port ----------------------------------------
stop_goosed # ensure no stale instance from a previous run
if [[ ! -f "$SECRETFILE" ]]; then
openssl rand -hex 32 > "$SECRETFILE"
fi
SERVER_SECRET="$(cat "$SECRETFILE")"
say "Starting goosed on https://127.0.0.1:$PORT (parallel-safe, own port)..."
GOOSE_PORT="$PORT" GOOSE_SERVER__SECRET_KEY="$SERVER_SECRET" \
nohup "$BIN" agent >"$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
# wait for it to listen
for _ in $(seq 1 30); do
if grep -q "listening on" "$LOGFILE" 2>/dev/null; then break; fi
sleep 0.5
done
grep -q "listening on" "$LOGFILE" 2>/dev/null || {
warn "goosed did not report 'listening' yet. Last log lines:"; tail -20 "$LOGFILE"; die "startup failed"
}
# ----- 4. open the tunnel ---------------------------------------------------
say "Opening mobile tunnel..."
RESP="$(curl -sk -X POST "https://127.0.0.1:$PORT/tunnel/start" \
-H "X-Secret-Key: $SERVER_SECRET")"
extract() { printf '%s' "$RESP" | sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p"; }
TUNNEL_URL="$(extract url)"
TUNNEL_SECRET="$(extract secret)"
[[ -n "$TUNNEL_URL" && -n "$TUNNEL_SECRET" ]] || {
warn "Could not start tunnel. Response was: $RESP"; die "tunnel start failed"
}
# ----- 5. show pairing details ----------------------------------------------
# This is the exact payload the desktop used to encode into the pairing QR code.
PAIR_JSON="{\"url\":\"$TUNNEL_URL\",\"secret\":\"$TUNNEL_SECRET\"}"
# urlencode for the deeplink
PAIR_ENC="$(printf '%s' "$PAIR_JSON" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read()))' 2>/dev/null || printf '%s' "$PAIR_JSON")"
DEEPLINK="goosechat://configure?data=$PAIR_ENC"
cat <<EOF
$(say "Tunnel is UP. Pair your goose mobile app with these details:")
Tunnel URL : $TUNNEL_URL
Secret : $TUNNEL_SECRET
To pair, either:
* Enter the URL + Secret manually in the mobile app, OR
* Generate a QR code from this deeplink and scan it with the app:
$DEEPLINK
EOF
# Optional: render a QR in the terminal if 'qrencode' is installed.
if command -v qrencode >/dev/null 2>&1; then
say "QR code (scan with the goose mobile app):"
qrencode -t ANSIUTF8 "$DEEPLINK"
else
warn "Install 'qrencode' (brew install qrencode) to render a scannable QR here."
fi
cat <<EOF
Keep this terminal open (goosed runs in the background, logs: $LOGFILE).
When you're done: $0 --stop
Notes:
* Runs alongside your normal goose (own port $PORT, same config/SQLite).
* If the mobile app can't connect, check the log: tail -f "$LOGFILE"
* Uninstall: $0 --stop && rm -rf "$WORKDIR"
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment