- Without the maintainer's key, Android compatibility reports stay on the Mac and the UI says so; the shared database is never contacted. - Remove personal infrastructure details from scripts and docs: the Drive folder and gog wrapper now come from the environment, and the Chromium build host is required instead of defaulted. - Add an MIT license, tester instructions in the README, and bump to 0.2.0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
16 KiB
Steam Frame ↔ Mac
Frame Control is a Mac app for managing a Valve Steam Frame (standalone VR headset: SteamOS 3, Arch-based, arm64, Snapdragon 8 Gen 3) over SSH: live headset view, battery and status, your Steam library, Android (Lepton) apps, file and clipboard transfer. This repo also holds the scripts behind it and field notes on how the Frame's software works, all aimed at as little typing on the headset's virtual keyboard as possible.
It's an unofficial hobby project, not affiliated with Valve.
Status: written 2026-09-25 and checked against a real Frame the same day
(SteamOS 0.3.0, variant vr, build 20260922). The Frame Control Mac app
and most scripts are verified on the device. The scripts table below marks
each one, and docs/open-questions.md
lists what's still unchecked.
Trying it out
You need:
- A Steam Frame with Developer Mode on (next section; it's a toggle).
- A Mac with Apple Silicon (M1 or later). Tested on macOS 26. There's no Intel build.
python3on the Mac (xcode-select --installprovides it).- Optional:
adbfor Android apps (brew install android-platform-tools).
Steps:
- Download the DMG from the latest release, open it and drag Frame Control to Applications.
- The app isn't notarized (no paid Apple developer account), so macOS will
say it's damaged or can't be checked. Clear the download quarantine once:
xattr -dr com.apple.quarantine "/Applications/Frame Control.app" - Open it. With no
frameSSH alias yet, it offers to run the connection setup in Terminal. That asks for the Developer Mode password once, then uses a key from then on.
Feedback: please open a
GitHub issue with what you
tried, your SteamOS build (Steam Settings → System) and the server log
(Frame → Show Server Log, at ~/Library/Logs/Frame Control/server.log).
Features are marked verified or not below; the unverified ones are the
most useful to hear about.
What it changes on your Frame: only what you click. Installs go to your
user account (--user Flatpaks, Lepton instances, Steam downloads), and
nothing needs sudo except the power buttons. On the Mac it adds a Host frame entry to ~/.ssh/config and a key at ~/.ssh/id_ed25519_frame.
Minimum typing on the headset
Valve's own developer docs say SSH, ADB, and RDP are all turned on through a
UI toggle. You don't need a terminal, passwd, or systemctl. The only
thing you type on the headset is a password you choose.
On the Frame:
- Steam Settings → System → Enable Developer Mode (a toggle, no typing).
- Scroll down to the Developer section and click Set User Password. Type a password. This is the only thing you type on the headset. Pick something short, because you'll type it once more on the Mac and then never again.
- (Optional, no typing) Note the IP address from Quick Settings or
Steam Settings → Internet, in case
frame.localdoesn't resolve. - (Optional) Check Steam Settings → System → Hostname. Leaving it as
framemeans the scripts work without any extra setup.
On the Mac:
To use the scripts from a checkout instead of the app:
git clone https://github.com/saphid/steam-frame.git && cd steam-frame
./scripts/connect.sh # or: ./scripts/connect.sh 192.168.1.50
ssh frame # passwordless from now on
connect.sh does four things:
- finds the headset (
frame.local, thenframe, or the IP/host you pass in) - creates a dedicated key (
~/.ssh/id_ed25519_frame) - adds a
Host frameblock to~/.ssh/config - runs
ssh-copy-id, which asks for the Developer Mode password once
Run ./scripts/connect.sh --harden later if you want to turn off SSH password
logins.
Sources: Valve: Setting up your Steam Frame for development, Valve: Steam Frame Debugging (both confirmed on Steam Frame, Valve official).
Fallback, only if the Developer Mode toggle doesn't give you SSH. From the
Mac, run ./scripts/serve-bootstrap.sh. It prints a one-liner of about 30
characters, like curl -fsS mac.local:8765|bash, to type into Konsole on the
Frame's Linux desktop. The script it serves installs your Mac's public key and
enables sshd. See docs/ssh.md.
Recommended options
| Goal | Recommended | Confidence |
|---|---|---|
| Shell on the Frame | ssh frame (user steamos) |
Confirmed (Valve docs) |
| See/control the Frame from the Mac | Steam Link for macOS → connect to frame (Valve names this). Alternatives: RDP to xrdp with Microsoft Windows App for the Linux desktop, or adb/scrcpy for the Android (Lepton) layer only |
Steam Link and xrdp confirmed on Frame; the Mac RDP client is inferred |
| Show the Mac's desktop inside the Frame | macOS Screen Sharing (built-in VNC) → Remmina (Flatpak, aarch64) on the Frame's Linux desktop, installed over SSH | Inferred: each piece is documented, but the combination hasn't been tested on a Frame |
| File transfer | scp / rsync over the frame alias (scripts/push.sh) |
Verified (rsync is on the image) |
| Paste Mac clipboard into the headset | scripts/paste-to-frame.sh (pbpaste → ssh → Klipper over D-Bus), or the clipboard sync in an RDP session |
Verified (script); RDP untested |
Details: docs/ssh.md, docs/streaming.md, docs/file-transfer.md, docs/open-questions.md. For how the Frame's software fits together, see docs/how-the-frame-works.md.
Windows anywhere in the room
The in-headset Linux desktop is a single 1280×800 panel, and its windows can't leave it. Each Steam app, though, gets its own SteamVR panel. That also works for any Linux app tagged with an app id of its own:
./scripts/panel-on-frame.sh konsole
./scripts/panel-on-frame.sh mac-screen # the Mac's screen, in its own panel
Then use the SteamVR dashboard's Float in World, Move and Size controls to place each panel. See docs/panels.md.
Frame Control (Mac app)
As of 2026-09-25 no other Mac app manages the Frame end to end. Stream Frame (macOS 14+, free) records and screenshots the headset over SSH. FrameDrop sideloads but is Windows-only. Steam Link views the headset. Frame Control is a Mac app over the scripts below. Install it from the DMG (see Mac app), or run the same UI in a browser without packaging:
./scripts/frame-ui.sh # opens http://127.0.0.1:47810 in its own window
- Headset view: what the lenses show, as SteamVR composites it (the room,
floating panels, dashboard and controllers). Shows the left eye, like pointing
a camera into one lens, or both eyes, as a single shot; saves as PNG. Live
is 720p video at about 30 fps:
ffmpegon the Frame encodes SteamVR's headset-view device (/dev/video99) to H.264 over SSH, and the page decodes it with WebCodecs. Live video is one eye; Capture still gets both. The viewer fits the whole frame; zoom with − / + (or scroll, or double-click), drag to pan,0to fit,Ffor full screen. Capture uses OpenVR'sIVRScreenshotsAPI through Pythonctypes(ui/frame_vrshot.py). Nothing extra is installed on the Frame (SteamOS shipsffmpeg). Desktop panel captures gamescope's flat layer instead. - Battery with charging state: charge rate in watts, time to full or empty, charger type and wattage (for example USB-C PD 20 W), and battery temperature
- Storage, memory, temperature, Wi-Fi, uptime, and whether SteamVR, the desktop, Lepton and xrdp are running
- Library shelf with Steam cover art and a Play button (
steam://rungameid) - Get games: every game you own with its Steam Frame rating (Verified,
Playable, Unsupported, Unknown). Install on Frame downloads it to the headset
with live progress. Search the Steam store with prices and Frame ratings; Buy
opens the store page in your browser, or Store on Frame opens it in the
headset. It drives the Frame's own Steam client through its DevTools port;
see
docs/steam-games.md - Volume and mute (
wpctl) - Android apps: search about 4,500 F-Droid apps rated for the Frame, install
one with a click as its own Lepton instance (it keeps its data and shows in the
Steam library), then launch, stop, test or remove it. Report an APK records whether any APK
worked (F-Droid or not: pick a file, type a package, or use an installed app). Your
reports are saved on your Mac and change the verdicts you see. They aren't
uploaded anywhere: the shared database is maintainer-only for now (see
compat-db/README.md) - Android display: pick a running Lepton instance (by the app in it) and set
its resolution (Native 1920×1080, or Sharp 2560×1440 with density scaled to
match), UI scale (Smaller / Default / Larger, or an exact dpi) and text size
(0.85–1.3×) over ADB (
wm size,wm density,font_scale). Reset puts all three back. Whether the settings survive the app relaunching is untested - Drag and drop files to
~/Downloads;.apkfiles install as their own Android app - Send typed text, or the Mac clipboard, to the Frame clipboard
- Install and remove Flatpaks (quick picks: Moonlight, Firefox, VLC, Remmina)
- One-click SSH or SFTP in Terminal, Steam Link, and Windows App (RDP). Sleep, restart and shut down open Terminal because SteamOS asks for the sudo password over SSH.
The server is Python stdlib only and listens on 127.0.0.1. It rejects requests
with a non-local Host header, and any /api/ request without a custom
header, so other websites can't drive it or read captures. It keeps a single multiplexed SSH connection open, so
status and each capture take about 0.3s. Headset captures are deleted from the
Frame as soon as they're copied, because they show everything on screen,
including anything private. The look follows the Steam client: its palette,
Motiva Sans (loaded from Valve's CDN), portrait library capsules and green
Play buttons. Verified on the Frame 2026-09-25: status and charging details,
both capture modes (headset view while in use, and a blank frame in standby,
which the UI labels), clipboard, volume, file push, and input validation. Not yet exercised from the UI: Launch, Flatpak
install/remove, APK drop, and the power buttons. Each of these calls a
command or script that was verified separately.
Mac app
app/ wraps the same UI as a standalone Mac app (Electron). The app bundles
ui/, scripts/, frame/android/ and the rated catalogue from apk-catalog/.
It starts ui/server.py on a free loopback port and shows it in its own window.
The server stops when you quit the app. A prebuilt DMG for Apple Silicon is
attached to each GitHub release.
cd app
npm install
npm run dist # → app/dist/Frame Control-<version>-arm64.dmg (and a .zip)
npm start # run from the checkout without packaging
Open the DMG and drag Frame Control to Applications. You need python3 on
the Mac (Xcode Command Line Tools or Homebrew). The app reads PATH from your
login shell, so Homebrew's rsync and adb work when you launch it from
Finder. Each time it starts while there's no frame SSH alias, the app offers
to run connect.sh in Terminal. Frame → Set Up Connection… does the same
at any time. The Frame menu also shows the server log at
~/Library/Logs/Frame Control/server.log. Installing APKs needs adb
(brew install android-platform-tools). The F-Droid ratings are bundled with the app.
The build is ad-hoc signed and not notarized. A copy you build yourself opens
normally. A copy downloaded from GitHub Releases is quarantined; clear it with
xattr -dr com.apple.quarantine "/Applications/Frame Control.app". The first
time you use them, macOS asks to allow local network access (for SSH) and
control of Terminal (for SSH and power actions). Verified 2026-09-25:
installed from the DMG, launched from Finder, connected to the Frame, and
showed live status and the library.
Scripts
| Script | Runs on | Purpose |
|---|---|---|
scripts/tailscale-on-frame.sh |
Mac → Frame | Install Tailscale in ~ as a userspace user service so frame works from anywhere; --uninstall (verified on the LAN) |
scripts/connect.sh |
Mac | Discover, set up key and ~/.ssh/config, copy key, optional --harden (verified; --harden untested) |
scripts/install-apps.sh |
Mac → Frame | Install Flatpaks (Remmina, Moonlight, …) on the Frame over SSH as --user (verified with Remmina) |
scripts/paste-to-frame.sh |
Mac → Frame | Send the Mac clipboard (or stdin) to the Frame clipboard (verified) |
scripts/install-apk.sh |
Mac → Frame | Install APKs, each as its own persistent Lepton instance with a Steam library shortcut (--dev: old ADB path into Lepton Development) (verified; see docs/apks.md) |
scripts/panel-on-frame.sh |
Mac → Frame | Start an app as its own floating VR panel, outside the desktop (verified: overlays created; in-headset placement not yet checked) |
scripts/run-on-frame.sh |
Mac → Frame | Start an app on the headset desktop, e.g. mac-screen opens Remmina straight into the Mac (verified) |
scripts/frame-ui.sh |
Mac | Start the Frame Control web UI (ui/server.py) and open it (verified) |
scripts/apk-catalog.sh |
Mac | Refresh the rated F-Droid catalogue that Frame Control's Android section shows (verified) |
scripts/compat-db-backup.sh |
Mac | Maintainer-only: back up the shared compatibility database locally and to Google Drive (verified) |
scripts/push-vr-video.sh |
Mac → Frame | Upload VR180/360 videos to ~/Videos/VR, linked into DeoVR's Proton prefix; --launch starts DeoVR (verified: upload and link; in-headset playback of local files not yet checked). See docs/vr-video.md |
scripts/push.sh |
Mac → Frame | rsync files to ~/Downloads (or a given path) on the Frame (verified) |
scripts/serve-bootstrap.sh |
Mac | Fallback: serve bootstrap-on-frame.sh with your public key embedded |
scripts/bootstrap-on-frame.sh |
Frame | Fallback: install the key and enable sshd |
Security notes
- With Developer Mode on,
sshd, ADB and xrdp are all reachable on your LAN. Each running Lepton (Android) instance opens its own ADB port in 5555–5599, listening on0.0.0.0rather than only loopback. This was seen on the device on 2026-09-25, so anyone on the network can reach it. Use trusted networks only, and turn Developer Mode off when you don't need it. - Frame Control reaches ADB and the Steam client's DevTools port (Frame
loopback
127.0.0.1:8080) only through SSH tunnels. The compatibility database key lives in the macOS Keychain and is never written to the repo. steamoshassudo, protected by the same Developer Mode password. Once you've switched to key auth, a short password still protectssudoand RDP, so pick one that isn't trivially guessable.- Don't port-forward 22, 3389, or 5555–5599 from your router. For remote access,
use Tailscale:
scripts/tailscale-on-frame.sh(no sudo). In its userspace mode every Frame port is reachable from your tailnet, including Steam's DevTools on loopback 8080; see docs/tailscale.md.
Development
python3 -m unittest discover -s tests # server guards, validation, Steam helpers; no headset needed
cd app && npm install && npm run dist # build the DMG
GitHub Actions runs the tests on Python 3.9, which is the oldest python3 the app
may find (Xcode Command Line Tools), plus syntax checks for every script and the
Electron main process (.github/workflows/checks.yml). Anything that touches the
headset is verified by hand against a real Frame, and the docs label it
verified or inferred.
License
MIT. Steam, Steam Frame and SteamVR are trademarks of Valve Corporation. This project isn't affiliated with or endorsed by Valve.
