On the Frame, Steam refused to register titles whose id had a hyphen (fc-smoke-exe) with "missing/invalid arguments", and registered the same program as FCSmokeProbe (headset smoke test, 2026-09-27, BUILD_ID 20260922.6101926). Valve's client only allows ^[A-Za-z_][A-Za-z0-9_.]+$. title_id now makes ids of letters, digits and _, not starting with a digit, 2 to 64 long; new installs are checked against that, while titles already on the Frame are still listed, launched and removed. Steam's error text is trimmed before it's quoted, and the "install it again with Steam running" hint only follows a Steam-not-running error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9.7 KiB
Sideloading Linux and Windows games
A game you have as files (an itch.io download, your own build, a DRM-free release) can go into the Frame's Steam library without a Steam store page. Frame Control uses the same path as Valve's SteamOS Devkit Client: the title becomes a Steam Devkit Game, with a runtime (Proton or a Steam Linux Runtime) chosen from the program itself.
For Android APKs, see apks.md instead.
Status: nothing here has run on a headset yet. Every device-side step is
inferred from Valve's steamos-devkit source (release v0.20260925.1). The
local steps (reading the zip, picking the program and runtime, building the
request) are covered by tests/test_frame_titles.py.
Using it
Drop a game's .zip, folder or .exe on Send to Frame. (Folders need the
desktop app, which knows where a dropped folder lives; in a plain browser, zip
it.) A dialog shows:
- Name: what Steam shows. Steam uses the title id as the name, so it's
limited to letters, digits and
_, and can't start with a digit; the dialog shows the result. - Launches: the program picked to start the game, with the other candidates in the list.
- Runtime: picked from the program, see below. Windows programs can switch between Proton Experimental and Proton (stable).
Install copies it to the Frame and registers it with Steam; progress shows in the bar and the activity log. Sideloaded titles lists what's installed, with Launch and Remove. Copy to ~/Downloads instead keeps the old behaviour for a zip that isn't a game.
From a terminal:
python3 ui/frame_titles.py inspect Game.zip # what would be installed, no headset needed
python3 ui/frame_titles.py install Game.zip [--name N] [--exe REL] [--runtime R]
python3 ui/frame_titles.py list | launch ID | remove ID
Choosing the runtime
The program's header decides, not its file name:
| Program | Runtime (Steam compat tool) | steam_play |
Confidence |
|---|---|---|---|
Windows .exe, x86-64 (PE machine 0x8664) |
proton-experimental |
1 | Inferred: ARM64 Proton runs x86-64 code through FEX |
Windows .exe, 32-bit x86 (0x14c) or ARM64 (0xaa64) |
proton-experimental |
1 | Inferred |
Linux ELF, aarch64 (e_machine 0xB7) |
SteamLinuxRuntime_4-arm64 |
0 | Verified: starts, but natively (see below) |
Linux ELF, x86-64 (0x3E) |
SteamLinuxRuntime_4 |
0 | Verified not to start: the runtime isn't installed (see below) |
| Shell script | the runtime of the Linux binary beside it, else SteamLinuxRuntime_4-arm64 |
0 | Guess |
| Anything else (32-bit Linux, other CPUs, DLLs, data) | refused with a message |
Proton Experimental is the default rather than stable because the Frame's ARM64 Proton and FEX stack is new and Proton fixes reach Experimental first. If a game misbehaves, reinstall it with Proton (stable).
The aliases and settings are the ones Valve's client sends: RUNTIME_ALIASES
in devkit_client/__init__.py, and gui2._update_game, which sets
steam_play=1, steam_play_debug=0, steam_play_debug_version=2019 for Proton
and steam_play=0 otherwise, plus compat_tool=<alias>. Valve's client only
offers SteamLinuxRuntime_4-arm64 and Lepton when the device reports itself
as Deckard (the Frame).
Picking the program
ui/frame_titles.py reads every file's header: ELF executables (PIE ones are
told from shared libraries by their PT_INTERP segment), PE executables (not
DLLs) and scripts with #!. A zip with a single top-level folder is treated
as that folder. Candidates are ranked by:
- Not a helper: names like
UnityCrashHandler64,CrashReportClient,*setup*,unins*,vc_redist*,dxsetup,*prereq*, and anything under_CommonRedist,Redist,DirectXorEnginego last. - Platform: native ARM64 Linux, then Windows x86-64, then x86-64 Linux, then other Windows builds.
- Name: a program named like the zip or folder (build words such as
-linux-arm64or_v1.2are dropped from the name). - Depth, then size: Unreal's top-level
Game.exebeatsGame/Binaries/Win64/Game-Win64-Shipping.exe.
A top-level shell script beats a Linux binary one folder down (run.sh +
bin/game); a binary next to a script wins. The list in the dialog lets you
pick another.
What happens on the Frame (inferred)
- Tools.
frame/devkit-utils/(Valve's scripts, vendored unmodified, MIT) is copied to~/devkit-utils, where Valve's client puts it, unless the stamp file there already matches. Files are merged, not replaced, so a newer copy from Valve's client keeps its extra files. - Folder.
python3 ~/devkit-utils/steamos-prepare-upload --gameid IDmakes~/devkit-game/IDand prints{user, directory}. - Copy. The files go there with
rsync -a --deleteon macOS and Linux, orscp -rinto a fresh folder that then replaces it on Windows. Thenchmod -R 755, the modes Valve's client gives an upload. - Register.
python3 ~/devkit-utils/steam-client-create-shortcut --parms JSONwith{gameid, directory, argv: [target], env: {}, settings, clear_settings, force_appid: "", lepton_args: ""}. It writesID-argv.json,ID-env.jsonandID-settings.jsonnext to the folder, then sendscreate-shortcutto the running Steam client over~/.steam/steam.pipe(authenticated by~/.steam/steam.token) and waits up to 5 s for Steam's answer file. Itserror, for example "The Steam client is not running", is shown as the install error. The files stay, so installing again with Steam running finishes the job. - Launch is
steam-devkit-rpc run-game gameid=ID. Remove issteamos-delete --delete-title ID, which deletes the folder and has Steam drop shortcuts with no folder. Frame Control then removes theID-*.jsonfiles that Valve's script leaves behind.
Frame Control also writes ~/devkit-game/ID-framecontrol.json (name, source
file, target, runtime, size). Sideloaded titles lists every folder in
~/devkit-game, including titles uploaded with Valve's client.
argv is one string, as in Valve's client (the start command may carry
arguments), so a program path with spaces is sent in double quotes. How Steam
splits that string is not checked.
Safety
- Zips are unpacked on your computer first. Entries with absolute paths,
.., drive letters or:anywhere in the path, or links that point outside the zip (or at a folder they're in) are refused. So are zips over 64 GB unpacked, over 200,000 entries, more than 200× compressed past 1 GB, or bigger than the free space. - No symlink is created while unpacking, so no write can be redirected
through one. A link to a file inside the zip (
libfoo.so.1 → libfoo.so.1.2) becomes a copy of that file, which also works on Windows. Links to folders, loops and dangling links are left out. - A dropped folder that contains symlinks (or Windows junctions) is copied on your computer first,
with the same rule, because
scp -rwould follow a link out of the folder and upload whatever it points at. - Installs run one at a time, and Remove is refused while one runs.
- The title id is limited to letters, digits and
_, doesn't start with a digit (one that would gets_in front), and is 2 to 64 characters. That's what Steam'screate-shortcutaccepts: on the Frame it refusedfc-smoke-exewithmissing/invalid argumentsand registered the same program asFCSmokeProbe(2026-09-27, BUILD_ID 20260922.6101926), and Valve's client only allows^[A-Za-z_][A-Za-z0-9_.]+$. Valve's scripts also pass the id to a shell (steamos-deleterunsrm -ron it). Valve's reserved sideload names (steam,steamvr, and theirdeckardforms, which would replace the Steam client itself) get_gameadded. - Nothing needs
sudo; everything goes to your home folder on the Frame. - In the app, a dropped folder is read from its local path by the app's own server, which only accepts requests from its own page (see frame-control.md).
Checked on a headset
Tested 2026-09-26 on a Frame (BUILD_ID 20260922.6101926) with small static test
programs and PuTTY's official 64-bit putty.exe, through both the command line
and the app (inspect, install job, ▶, Remove, and install links):
create-shortcutregisters a title; it shows in the Steam library and in Sideloaded titles, and Steam maps it to the chosen compat tool.steam-devkit-rpc run-gamestarts it (Steam logsdevkit run-game: started devkit game "<id>"), and Remove (steamos-delete) deletes the files, the shortcut and the Proton prefix.- A quoted path in the start command is fine: Steam runs
proton waitforexitandrun "/home/steamos/devkit-game/<id>/<exe>". - An x86-64 Windows
.exeruns under Proton 11 (stable) through FEX (ARM64EC) inside the Steam Linux Runtime 4.0 ARM64 container; PuTTY stayed up. Proton Experimental wasn't installed at the time (it was downloading), so it's untested. A Go-built x86-64 test program crashed inlibarm64ecfex.dll(a FEX limitation with that program, not the sideloading). - An aarch64 build runs natively, not in
SteamLinuxRuntime_4-arm64: Steam records the mapping (CompatToolMapping,compat_log.txt) but launches the devkit title without the runtime's_v2-entry-pointprefix. Fine for a self-contained build; a build that needs the runtime's libraries may not start. - An x86-64 Linux build doesn't start: Steam logs
Tool 4183110 "Steam Linux Runtime 4.0" is found for appID …, but is not installed, and the Frame doesn't install that x86-64 runtime for a devkit title (asteam://install/4183110request did nothing). - Whether these titles open as flat panels or need anything VR-specific.