Steam Frame from a Mac: research docs and helper scripts

README with the minimum-typing checklist (Developer Mode toggle + Set User
Password; the rest runs from the Mac), docs for SSH, streaming, file
transfer, and open questions with sourced confidence levels, plus Mac-side
zsh helpers and a fallback headset bootstrap.

Scripts are UNTESTED against hardware: checked with zsh -n / bash -n /
shellcheck only. Two SWE-2 Max read-only review passes (devin -p --model
swe-2-max); verified findings fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-25 15:38:43 +10:00
commit 1f6115b789
11 files changed
+700

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Runs ON the Steam Frame (fallback path only; normally Developer Mode's
# toggle + "Set User Password" is enough and this is not needed).
# Served by scripts/serve-bootstrap.sh, which substitutes the public key.
#
# UNTESTED against real hardware. Idempotent.
set -eu
KEY='__PUBKEY__'
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
touch "$HOME/.ssh/authorized_keys"
chmod 600 "$HOME/.ssh/authorized_keys"
if grep -qxF "$KEY" "$HOME/.ssh/authorized_keys"; then
echo "key already present"
else
echo "$KEY" >> "$HOME/.ssh/authorized_keys"
echo "key added"
fi
echo "Enabling sshd. If sudo asks for a password you never set, press Ctrl-C,"
echo "set one in Steam Settings > Developer > Set User Password (or run: passwd),"
echo "then re-run the same one-liner."
sudo systemctl enable --now sshd
echo
echo "sshd: $(systemctl is-active sshd) user: $(id -un) host: $(hostname)"
ip -4 -brief addr show scope global 2>/dev/null || true
echo "Now on the Mac: scripts/connect.sh"
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env zsh
# Mac-side: find the Steam Frame, create a key, add a `Host frame` alias to
# ~/.ssh/config, copy the key, and optionally disable SSH password logins.
#
# UNTESTED against real hardware. Idempotent: safe to re-run.
#
# Usage:
# scripts/connect.sh [HOST_OR_IP] # set up key + alias
# scripts/connect.sh [HOST_OR_IP] --harden # also disable password auth
#
# Env: FRAME_USER (default steamos), FRAME_ALIAS (default frame).
set -euo pipefail
FRAME_USER=${FRAME_USER:-steamos}
FRAME_ALIAS=${FRAME_ALIAS:-frame}
KEY="$HOME/.ssh/id_ed25519_frame"
CONFIG="$HOME/.ssh/config"
BEGIN_MARK="# >>> steam-frame ($FRAME_ALIAS) >>>"
END_MARK="# <<< steam-frame ($FRAME_ALIAS) <<<"
harden=0
host_arg=""
for arg in "$@"; do
case "$arg" in
--harden) harden=1 ;;
-h|--help) sed -n '2,11p' "$0"; exit 0 ;;
*) host_arg="$arg" ;;
esac
done
port_open() {
# nc resolves through the system resolver (including mDNS for .local).
nc -z -G 3 "$1" 22 >/dev/null 2>&1
}
pick_host() {
local candidates=()
[[ -n "$host_arg" ]] && candidates+=("$host_arg")
candidates+=("$FRAME_ALIAS.local" "$FRAME_ALIAS")
local h
for h in "${candidates[@]}"; do
if port_open "$h"; then
print -r -- "$h"; return 0
fi
print -u2 " - $h: not resolvable or port 22 closed"
done
return 1
}
print "==> Looking for the Steam Frame"
if ! HOST=$(pick_host); then
print -u2 "Could not reach the Frame on port 22."
print -u2 "Check: Developer Mode on + user password set; same Wi-Fi; no client isolation."
print -u2 "Then re-run with the IP from Quick Settings: scripts/connect.sh 192.168.x.y"
exit 1
fi
print " found: $HOST"
print "==> SSH key"
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
if [[ ! -f "$KEY" ]]; then
ssh-keygen -q -t ed25519 -N '' -C "mac->steam-frame" -f "$KEY"
print " created $KEY"
else
print " exists: $KEY"
fi
print "==> ~/.ssh/config alias '$FRAME_ALIAS' -> $HOST"
touch "$CONFIG" && chmod 600 "$CONFIG"
tmp=$(mktemp)
# Drop any previous managed block, then PREPEND a fresh one: ssh uses the first
# value it sees per option, so this block must precede any other "Host frame"
# or "Host *". The trailing "Host *" returns the rest of the file to global scope.
awk -v b="$BEGIN_MARK" -v e="$END_MARK" '
$0==b {skip=1; next}
$0==e {skip=0; next}
!skip {print}
' "$CONFIG" > "$tmp"
{
print -r -- "$BEGIN_MARK"
print -r -- "Host $FRAME_ALIAS"
print -r -- " HostName $HOST"
print -r -- " User $FRAME_USER"
print -r -- " IdentityFile $KEY"
print -r -- " IdentitiesOnly yes"
print -r -- " ServerAliveInterval 30"
print -r -- "Host *"
print -r -- "$END_MARK"
cat "$tmp"
} > "$CONFIG"
rm -f "$tmp"
print "==> Checking key login"
if ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true 2>/dev/null; then
print " key login already works"
else
print " copying key (enter the Developer Mode password once)"
ssh-copy-id -i "$KEY.pub" -o IdentitiesOnly=yes "$FRAME_USER@$HOST"
ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true \
|| { print -u2 "Key login still failing after ssh-copy-id."; exit 1; }
print " key login OK"
fi
if (( harden )); then
print "==> Disabling SSH password auth (sudo password asked on the Frame)"
# shellcheck disable=SC2016
if ! ssh -t "$FRAME_ALIAS" '
set -e
grep -Eiq "^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf" /etc/ssh/sshd_config \
|| { echo "sshd_config has no sshd_config.d include; not hardening."; exit 1; }
printf "PasswordAuthentication no\nKbdInteractiveAuthentication no\n" \
| { sudo mkdir -p /etc/ssh/sshd_config.d; sudo tee /etc/ssh/sshd_config.d/01-frame-keys-only.conf >/dev/null; }
sudo sshd -t
sudo systemctl reload sshd
echo "password auth disabled"
'; then
print -u2 "!! Hardening failed. If the drop-in was written, it will disable password SSH"
print -u2 "!! on the next sshd restart. To undo it:"
print -u2 "!! ssh $FRAME_ALIAS 'sudo rm -f /etc/ssh/sshd_config.d/01-frame-keys-only.conf'"
exit 1
fi
if ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true; then
print " key login still OK after hardening"
else
print -u2 "!! Key login FAILED after hardening. Password SSH is now off."
print -u2 "!! Recover via RDP or 'adb shell' (USB-C), then run:"
print -u2 "!! sudo rm /etc/ssh/sshd_config.d/01-frame-keys-only.conf && sudo systemctl reload sshd"
exit 1
fi
fi
print "\nDone. Try: ssh $FRAME_ALIAS"
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env zsh
# Mac-side: install Flatpaks on the Steam Frame over SSH (per-user, so they
# survive SteamOS updates and need no sudo / steamos-readonly changes).
#
# UNTESTED against real hardware. Idempotent.
#
# Usage:
# scripts/install-apps.sh remmina [--vnc-host my-mac.local]
# scripts/install-apps.sh moonlight
# scripts/install-apps.sh org.example.SomeApp # any Flathub app ID
#
# --vnc-host pre-seeds a Remmina profile pointing at the Mac's built-in
# Screen Sharing (VNC, port 5900) so nothing needs typing in the headset.
set -euo pipefail
FRAME_ALIAS=${FRAME_ALIAS:-frame}
vnc_host=""
apps=()
while (( $# )); do
case "$1" in
--vnc-host) vnc_host=${2:?--vnc-host needs a hostname}; shift 2
[[ "$vnc_host" =~ '^[A-Za-z0-9.-]+$' ]] || { print -u2 "Bad hostname: $vnc_host"; exit 2; } ;;
-h|--help) sed -n '2,13p' "$0"; exit 0 ;;
remmina) apps+=(org.remmina.Remmina); shift ;;
moonlight) apps+=(com.moonlight_stream.Moonlight); shift ;;
[A-Za-z]*.*[A-Za-z0-9_]) [[ "$1" =~ '^[A-Za-z0-9_.-]+$' ]] || { print -u2 "Bad app ID: $1"; exit 2; }; apps+=("$1"); shift ;;
*) print -u2 "Unknown app '$1' (use remmina, moonlight, or a Flathub app ID)"; exit 2 ;;
esac
done
if (( ${#apps} == 0 )) && [[ -z "$vnc_host" ]]; then
sed -n '2,13p' "$0"; exit 2
fi
if (( ${#apps} )); then
print "==> Installing on $FRAME_ALIAS: ${apps[*]}"
ssh "$FRAME_ALIAS" "
set -e
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y flathub ${(j: :)${(@q)apps}}
"
fi
if [[ -n "$vnc_host" ]]; then
print "==> Writing Remmina profile for vnc://$vnc_host"
ssh "$FRAME_ALIAS" "
set -e
d=\$HOME/.var/app/org.remmina.Remmina/data/remmina
mkdir -p \"\$d\"
cat > \"\$d/mac-screen-sharing.remmina\" <<'EOF'
[remmina]
name=Mac Screen Sharing
protocol=VNC
server=$vnc_host:5900
colordepth=32
quality=9
viewonly=0
showcursor=1
EOF
echo \"wrote \$d/mac-screen-sharing.remmina\"
"
print "On the Mac: System Settings > General > Sharing > Screen Sharing (i) >"
print " enable 'VNC viewers may control screen with password' and set one."
fi
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env zsh
# Mac-side: put text on the Steam Frame desktop clipboard.
#
# UNTESTED against real hardware. Assumes the in-headset desktop is a Plasma
# session owned by the SSH user; prints diagnostics if that assumption fails.
#
# Usage:
# scripts/paste-to-frame.sh # sends the Mac clipboard (pbpaste)
# some-cmd | scripts/paste-to-frame.sh -
set -euo pipefail
FRAME_ALIAS=${FRAME_ALIAS:-frame}
# Runs on the Frame. Clipboard text arrives on stdin. setsid keeps the
# clipboard-serving process alive after the SSH session closes.
remote=$(cat <<'EOF'
set -u
tmp=$(mktemp)
cat > "$tmp"
rt=/run/user/$(id -u)
sock=$(ls "$rt" 2>/dev/null | grep -E '^wayland-[0-9]+$' | head -n 1)
if [ -n "$sock" ] && command -v wl-copy >/dev/null 2>&1 \
&& XDG_RUNTIME_DIR=$rt WAYLAND_DISPLAY=$sock setsid wl-copy < "$tmp" >/dev/null 2>&1; then
echo "copied via wl-copy ($sock)"
elif command -v xclip >/dev/null 2>&1 \
&& DISPLAY=:0 setsid xclip -selection clipboard -i < "$tmp" >/dev/null 2>&1; then
echo "copied via xclip (DISPLAY=:0)"
else
echo "clipboard copy failed; diagnostics:" >&2
echo " runtime dir: $(ls "$rt" 2>&1 | tr '\n' ' ')" >&2
echo " wl-copy: $(command -v wl-copy || echo missing) xclip: $(command -v xclip || echo missing)" >&2
loginctl list-sessions --no-legend 2>&1 | sed 's/^/ session: /' >&2
rm -f "$tmp"; exit 2
fi
rm -f "$tmp"
EOF
)
b64=$(print -rn -- "$remote" | base64)
if [[ "${1:-}" == "-" ]]; then
ssh "$FRAME_ALIAS" "bash -c \"\$(echo $b64 | base64 -d)\""
else
pbpaste | ssh "$FRAME_ALIAS" "bash -c \"\$(echo $b64 | base64 -d)\""
fi
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env zsh
# Mac-side: copy a file or folder to the Steam Frame.
#
# UNTESTED against real hardware.
#
# Usage: scripts/push.sh SOURCE [REMOTE_DEST] (default dest: ~/Downloads/)
set -euo pipefail
FRAME_ALIAS=${FRAME_ALIAS:-frame}
src=${1:?usage: push.sh SOURCE [REMOTE_DEST]}
dest=${2:-Downloads/}
if ssh "$FRAME_ALIAS" 'command -v rsync >/dev/null'; then
rsync -a --progress "$src" "$FRAME_ALIAS:${(q)dest}" # remote shell parses the path
else
print -u2 "rsync not found on the Frame; falling back to scp"
scp -r "$src" "$FRAME_ALIAS:$dest" # modern scp uses SFTP: no remote shell parsing
fi
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env zsh
# Mac-side (fallback only): serve bootstrap-on-frame.sh over plain HTTP on the
# LAN, with this Mac's Frame public key embedded, and print the short
# one-liner to type in Konsole on the headset. Ctrl-C to stop.
#
# UNTESTED against real hardware. Serves only a public key; use on a trusted LAN.
set -euo pipefail
PORT=${PORT:-8765}
here=${0:A:h}
KEY="$HOME/.ssh/id_ed25519_frame"
if [[ ! -f "$KEY.pub" ]]; then
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
ssh-keygen -q -t ed25519 -N '' -C "mac->steam-frame" -f "$KEY"
fi
pub=$(<"$KEY.pub")
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
# index.html so the bare URL works; curl doesn't care about the name.
sed "s|__PUBKEY__|$pub|" "$here/bootstrap-on-frame.sh" > "$dir/index.html"
name="$(scutil --get LocalHostName 2>/dev/null || hostname -s).local"
ip=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)
print "Type ONE of these in Konsole on the Frame:"
print " curl -fsS $name:$PORT|bash"
[[ -n "$ip" ]] && print " curl -fsS $ip:$PORT|bash"
print "Serving from $dir on port $PORT (Ctrl-C to stop)..."
python3 -m http.server "$PORT" --directory "$dir"