Playhook is a background app that turns a removable drive into a console-style game
cartridge. It lives in the tray, detects when you insert a card carrying a game (a game.json
manifest), and pops up a game card. Press A on an Xbox gamepad (or click Play) and it
syncs your saves onto the PC, launches the game, tracks the playtime, then copies the saves
back to the card when you quit — so your progress travels with the card across machines.
It runs on Windows and on the Steam Deck / Linux (SteamOS), where the same Windows game cards launch through Proton — see Steam Deck.
The card can carry the game itself, an installer for heavy games
(Install mode), or just a pointer to a Steam app by
appid that Playhook installs, launches and uninstalls through your local Steam client
(Steam mode).
Cross-platform. Playhook runs on Windows, on the Steam Deck / Linux (SteamOS) and on macOS (Apple Silicon). On Linux the same Windows game cards run through Proton (via umu-launcher); native Linux/ELF games are not a target. macOS is a narrower port: native mac games and Steam mode, but no Windows
.exeand no install mode. See Steam Deck and macOS.
Security note. The card is untrusted input — every path in the manifest is validated against directory traversal and an allowlist before anything is read or written. See Preparing a card for the exact rules.
The Playhook home screen: the history carousel, with the selected game's hero art behind it. Ready-made cards — game.json, hero art and music — live in the Playhook Collection, whose UI you can also try right in the browser.
Grab the latest build from the Releases page. Four are published:
- NSIS installer (
.exe, recommended on Windows) — installs the app, configures autostart reliably, and updates itself automatically. - portable (
.exe) — runs without installation; no auto-update, autostart is best-effort. - AppImage — the Steam Deck / Linux build; it self-updates too. See Steam Deck for the setup.
- dmg (
-arm64.dmg) — the macOS build, Apple Silicon only. It does not self-update: grab a new dmg from this page when a version comes out. See macOS.
A couple of things to expect on first run on Windows:
- SmartScreen warning. The builds are not code-signed, so Windows SmartScreen will warn you the first time. This is expected — choose More info → Run anyway.
- Visual C++ Redistributable. If the app fails to start on a clean Windows install, install
the latest Visual C++ Redistributable (x64).
(
.NETis not required.)
And on macOS:
- Gatekeeper. The build is not signed with an Apple Developer ID, so the first launch is blocked.
On macOS 15+ the old right-click → Open trick no longer works for an unsigned app: open the app,
let it be refused, then go to System Settings → Privacy & Security and press Open Anyway next to
the message about Playhook. (The command-line equivalent is
xattr -dr com.apple.quarantine /Applications/Playhook.app.) - Removable-volume access. The first time Playhook looks at a card, macOS asks for permission to
read removable volumes. Decline it and your cards become invisible with no other symptom — the only
trace is a
permission deniedline in the log. Grant it again in System Settings → Privacy & Security → Files and Folders.
- Install (or unzip the portable build) and run it — it sits quietly in the tray.
- Insert a card that has a
game.jsonin its root (see below). - A game card window appears with the hero art and the game title (playtime stats live in the More ⋯ menu).
- Press A on the gamepad (or click Play) — saves sync and the game launches.
- Close the game — Playhook counts the time, updates stats, and syncs saves back to the card.
Don't want to hand-write a game.json? Open a game's More ⋯ → Customize and edit it right in the
launcher — see Settings and Customize.
- Playhook starts hidden in the tray. With no game card inserted, there is no window (unless you enable Always show the no-card screen in Settings).
- Insert a card with a valid
game.jsonand a window appears: the hero art as the background and a bottom bar with the Play button on the left, the game title in the middle and the More (⋯) button on the right (stateready). If the manifest has noheroImage, a bundled wallpaper — or your own, set in Settings — is used. - With more than one game to show, the launcher opens on the history carousel instead: a row of game cards you flip through with left/right. The games on the inserted card come first (each marked with a dot — those you can launch right now), followed by the games you have played on this device before. A opens the selected game's screen, B goes back to the row.
- Press A on the gamepad (the window is force-focused) or click Play.
- Saves are synced card → PC and the game launches; it takes the foreground over the launcher.
- When the game closes, Playhook counts the play time, updates the statistics, and syncs the saves PC → card. The game card window returns.
The More (⋯) button opens the Details menu — one right-side popup that carries everything besides Play. It always shows the game's stats (last played, playtime, launches) and, depending on the state, offers:
- Install / Uninstall — for install-mode and Steam-mode cards (an uninstalled game has no Play button at all — you start from here);
- Force close — while a game is running, kills it and still records the session and syncs saves;
- Remove from history — for a game you no longer have (its card is out and it is not a local game), drops it from the carousel along with the art copied for it. Saves and playtime stay: put the card back in and the game returns with its stats. Games you can play right now don't offer this item — they are rebuilt from their card / library every time it is read;
- System — a submenu with Shutdown / Reboot / Sleep (each behind a confirmation) and Minimize Playhook, which sends the window back to the tray. In Game Mode that last item is Close Playhook (a full quit) instead, since there is no tray to minimize into. It belongs to the launcher rather than to any one game, so it lives in the carousel's own More menu (System + Close), and a game's menu is only about that game. With no carousel to go up to — a single-game card, or the empty screen — it stays where it was: that menu is then the only one there is.
Every confirmation and every error uses that same popup; close it with B or a click on Close. If a launch fails, the reason appears there and you can simply retry.
Every game inserted into this device leaves a copy of its art and music in
%APPDATA%/playhook/library/ (~/.config/playhook/ on Linux, ~/Library/Application Support/playhook/
on macOS), so the carousel still shows the games you
have had once the card is out — pick one and you get its screen (title, stats, background and music),
with no Play button: there is nothing to launch without the card.
Flip through the row with left/right (hold to run through it), open a game with A, and step back to the row with B. Y hands the highlight over to the More button beside the row and back again — the row's own menu is the launcher's (System + Close) — and so does right on the last card, though only as a separate press: holding right runs to the end of the row and stops there. From the button, left and B both return to the cards. With a mouse: the wheel scrolls the row, a click selects a card, a second click opens it, and a right-click steps back. The row is the top level — the game's screen sits one step inside it, which is where More ⋯ and its actions live.
The row is ordered by how recently you touched a game — the later of "its card was inserted" and "you played it" — so a card you put in yesterday and never got around to starting still sits near the front. The games on the currently inserted card come first, ordered by when you last played them. The history keeps 40 games; beyond that the least recently touched are dropped, and the games on the inserted card are never evicted. To drop one yourself, open it and use Remove from history in the More ⋯ menu.
Use gridImage in game.json to control how a game looks in that row. It expects a 600x900 portrait
cover — the same format Steam uses, so SteamGridDB is the easiest place to
find one. Without it the card is cropped from the first heroImage.
Not every game lives on a card. A game that is already installed on this machine can be added to the launcher as a local game, and from then on it behaves like any other: its own hero art, carousel card, music, stats, save sync and Play button — with or without a card inserted. More ⋯ → Add game adds one from the launcher itself (pick "This PC" as the source); an existing one is edited the same way a card game's is, through More ⋯ → Customize.
Local games are stored in %APPDATA%/playhook/pc-games/ (~/.config/playhook/pc-games/ on Linux,
~/Library/Application Support/playhook/pc-games/ on macOS),
which is laid out exactly like a card: a game.json, an assets/ folder for the art and music you
pick (they are copied in, so moving or deleting the originals doesn't break anything), and a
saves/ folder for the save backups. The manifest is the same format, with one extra block and one
rule of its own:
pcreplacesexecutableand is mutually exclusive withinstallandsteam; everything else (args,runAsAdmin,watchProcesses, the timeouts,winetricks,umuGameId, the art and the music) works exactly as it does for a card game.
A local game can also be a Steam game installed on this PC — the second launch mode the library
accepts. Instead of pc, give it a steam block:
{
"schemaVersion": 1,
"id": "hades-steam",
"title": "Hades",
"steam": { "appid": 1145360 },
// Required in steam mode: Playhook cannot see Steam's own process tree, so it watches for these
// image names to know the game is running. Take the name from steamapps/common/<game>/ (or the
// game's SteamDB page) — it is the .exe even on the Deck, where the game runs under Proton.
"watchProcesses": ["Hades.exe"],
"heroImage": ["assets/hades-hero.jpg"],
// Optional. Under Proton the saves live inside Steam's own prefix, so use the %PREFIX% form here —
// Browse fills it in for you after the game has been run once.
"pcSavePath": "%APPDATA%/Hades"
}- The button follows Steam: Install while the game isn't installed (it opens Steam's download), Play once it is, and Uninstall hands the removal back to Steam. Installing or removing the game in Steam directly is picked up on its own, card or no card.
- Everything Steam owns stays Steam's: there is no
installblock, norunAsAdmin, and no Wine prefix of ours (the game runs in Steam's compatdata). pcandsteamare mutually exclusive — a local game is one or the other.- Saves are backed up by Playhook itself — there is no card to keep them on, so
saveOnCardis not allowed and the backup goes topc-games/saves/<id>/. If you later insert a card carrying the same game, the progress you made without it is copied onto the card on insertion. - Deleting the game from your disk doesn't delete it from Playhook. The card stays in the carousel with its art and stats, the status line reads Game files not found, and Play is hidden. Put the game back at the same path and it is playable again, saves included.
- If a card carries a game with the same
idas a local one, the card wins while it is inserted; the local entry is hidden until the card is removed. - Paths here are not portable: they are written in this machine's native form, since this library
never travels (a card's
game.json, by contrast, must work on both Windows and the Deck).
Draft games. A local game may be saved with no launch method at all — no pc, no steam. Use this
to fill in everything else (title, art, music, timings, watchProcesses) before you know how the game
will actually be started. A draft is visible in the carousel and stays fully editable through Customize,
but it has no dot, no Play button and no status line — the missing Play button says it. Save & Apply is
still available once you fill in pc or steam.
Moving a local game to a card. More ⋯ → Move to card… (a local game only) copies a game's
metadata, art, music and save backup onto a card you pick, and removes it from the PC library once the
write succeeds — the game itself, however, is not copied: put its files on the card yourself first
(under whatever relative path you're about to give executable), or the move is refused with a message
saying so. On the Customize screen this shows up as the form growing new fields once you have picked a
target card (an executable/install block and, if you want save sync, saveOnCard + pcSavePath) —
fill those in and Save the same way you would for an ordinary card game. Nothing is written anywhere
until you do; backing out (Back, or the popup that closes the launcher screen) leaves both sides exactly
as they were. If the target card isn't the one currently inserted, the move still succeeds — a
notification says so, and the game shows up on the carousel once that card is.
On the Steam Deck in Game Mode the launcher is started by a card being inserted, so local games are reachable there only if you open Playhook's tile yourself.
The empty screen (no card inserted, no history) reuses the same layout over the wallpaper: "Insert a game card", no Play button, and More offering just the System submenu (where Minimize Playhook lives).
When the launcher is hidden you can hold Start + Back on the gamepad to re-summon it (the hotkey can be turned off in Settings). It is intentionally ignored while a game is running — pulling the launcher over a running game only causes focus trouble.
Tray menu: Show launcher, Open logs, Open games folder, Quit — plus Add to Steam / Remove from Steam on the Steam Deck. Settings and the manifest editor are screens of the launcher itself, reached from More ⋯, so they work in Game Mode too.
Both are SCREENS of the launcher, opened from the More ⋯ menu — so they work with a gamepad, and on the Steam Deck they work in Game Mode as well as on the desktop.
- Updates — automatic mode (download and install / download, install manually / off), the pre-release (beta) channel, and a manual Check for updates with the current version.
- Appearance — theme (system / light / dark), language (system / English / Russian), and a custom background for the empty screen (any image; Reset restores the bundled wallpaper).
- General — the gamepad summon hotkey, keep the screen awake while the launcher is open, always show the no-card screen (instead of hiding to the tray), disable silent installer mode (see Install mode), and, on the Steam Deck, the Game Mode auto-launch on card insertion.
- Audio — the navigation sound set and the background ambience shipped with the app, a
volume slider for each, and an only global ambience switch that makes the app's ambience win over
whatever the card carries in
backgroundMusic. The navigation sounds always come from the chosen set: a card cannot supply its own. - Advanced — Open logs, Open games folder (the install-mode directory), Reset to defaults.
Settings live in settings.json next to the rest of the app state (%APPDATA%\playhook\ on Windows,
~/.config/playhook/ on Linux, ~/Library/Application Support/playhook/ on macOS); a missing or
corrupted file falls back to the defaults.
More ⋯ → Customize edits the game.json of the game you are looking at — the one on the inserted
card, or a local one — so you never have to edit JSON by hand. It is offered only for a game that is
available right now, because that is the only case where there is a file to reach.
- a form with sections Basics / Launch / Artwork / Saves / Audio / Advanced / Linux, whose rows follow the launch type you pick (a Steam game has no executable, an installer has no "move to PC" checkbox) — and Linux is dropped for a game installed on a Windows PC, which is never run through Proton;
- Browse for the executable, the installer, the hero backgrounds (up to 3), the 600x900 carousel
card image, the background music and the save folders — through the launcher's own file browser, which
a gamepad can drive (a picked PC save folder is converted back into a
%APPDATA%-style prefix automatically, and a file outside the card is rejected); - an on-screen keyboard for every text field, with English, Russian and symbol layouts — the Deck's own keyboard is not available to an app outside Steam. The caret goes anywhere in the value (click it, or the ◀ ▶ keys, or the arrows on a real keyboard), X held keeps deleting, and Paste pulls the system clipboard in (Ctrl+V too), filtered by whatever the field accepts;
- live validation: a problem is shown on the row that owns it, and Save stays unavailable until it is gone. On a multi-game card, a problem in ANOTHER game is reported as a line of its own — and does not block saving yours, since you cannot fix it from here anyway;
- Save & Apply writes the file and reloads the launcher immediately (or after you finish playing, if the game is running right now); Discard changes re-reads the file;
- Delete game removes it from the manifest — never its files, and never a local game's save backups. It is hidden while the game is running, and for the last game on a card (which would leave the card with no manifest at all).
Don't want to write one by hand? The Playhook Collection is a growing set of ready-made, verified manifests — hero art, music, save paths and all. Browse it, preview a game, and grab its
game.json+ assets for your card. Source and contribution guide: github.com/sevenns/playhook-collection.
Place a game.json in the root of the card. The paths
executable / heroImage / gridImage / saveOnCard are relative to the card root; pcSavePath is
absolute and must start with one of the allowed prefixes (see below).
The file holds either one game object (below) or an array of them — a card can carry several
games. The launcher opens on the history carousel and you switch by flipping through it; each game
keeps its own stats, saves and install state (they are keyed by id). One bad entry doesn't sink the
whole card — it is skipped (with a line in the log) and the rest still load; duplicate ids are
rejected, since the id keys the PC-side storage. Customize reports the problem per game.
{
"schemaVersion": 1,
"id": "hollow-knight", // stable id: key of the stats/pending-flush folder on the PC
"title": "Hollow Knight",
"executable": "game/hollow_knight.exe", // relative path to the .exe from the card root
"args": [], // launch arguments (optional)
"runAsAdmin": false, // launch elevated via UAC for .exe requiring admin (optional, default false)
"heroImage": "assets/hero.jpg", // window background: one path, or an array of UP TO 3 paths that cross-fade every minute (optional; falls back to a bundled wallpaper)
"gridImage": "assets/grid.jpg", // the game's card in the history carousel: a 600x900 portrait cover, as on SteamGridDB (optional; without it the card is cropped from the first heroImage)
"saveOnCard": "saves", // copy folder for saves on the card (relative to the root)
"pcSavePath": "%APPDATA%/Team Cherry/Hollow Knight", // where the game actually writes saves on the PC
"launchTimeoutSec": 30, // how long to wait for the process to appear (optional, default 30)
"killTimeoutSec": 60, // how long "Force close" waits for the game to die (optional, default 60)
"watchProcesses": ["Game-Win64-Shipping.exe"], // for launcher/wrapper games: track THESE process names, not the spawned launcher (optional)
"backgroundMusic": "audio/theme.ogg" // looping music while the window is visible (volume from Settings, optional)
}E:\
├─ game.json
├─ game\
│ └─ hollow_knight.exe
├─ assets\
│ └─ hero.jpg
└─ saves\ ← portable canonical copy of the saves
- After resolution,
executable/heroImage/gridImage/saveOnCardmust lie inside the card root —..and absolute paths are forbidden (otherwise the game won't launch and an error is shown). Theexecutablemust also exist on the card, or launch is rejected. pcSavePath— only from an allowlist of prefixes, with no traversal (..). Otherwise rejected:%DOCUMENTS%— the user's Documents folder, resolved via the system Known Folder API (app.getPath('documents')). Language- and OneDrive-independent: it returns the same physical path the game itself uses, so%DOCUMENTS%/My Games/...works on any machine without caring whether the folder is namedDocuments,Документы, or sits under OneDrive. Prefer this for games that save under Documents (most Bethesda titles, e.g. Fallout/Skyrim).%APPDATA%,%LOCALAPPDATA%,%USERPROFILE%— resolved from the corresponding environment variables (good for games that save under AppData).%LOCALLOW%— theAppData\LocalLowfolder, derived from%USERPROFILE%(it has no environment variable of its own). Common for Unity / Steam games (e.g. Valheim →%LOCALLOW%/IronGate/Valheim). Note:%APPDATA%isAppData\Roamingand is not a parent ofLocalLow— use%LOCALLOW%for LocalLow saves.
id— only[A-Za-z0-9._-](used as a folder name on the PC).runAsAdmin— settrueonly for an.exewhose embedded manifest requires administrator (a plain launch fails withEACCES/ERROR_ELEVATION_REQUIRED). Playhook then launches it elevated via a UAC prompt (ShellExecuteExrunas) and monitors it by process HANDLE instead oftasklist(a non-elevated app can't see an elevated process). Opt-in on purpose — Playhook never silently escalates an untrusted card's exe. On the Steam Deck / Linux there is no elevation under Proton, and macOS has no equivalent either, sorunAsAdmin: trueis a no-op (logged) rather than an error there — the same card stays valid on all three platforms.watchProcesses— for launcher / wrapper games, whereexecutablespawns a launcher that starts the game in a separate process and then exits (so watching the spawned pid would wrongly report "closed" the instant the launcher quits). List the game's own process image names here: Playhook still spawnsexecutable, but tracks the session by the presence of these names intasklist. Playtime starts when a watched process appears and ends when all of them are gone. When omitted, behaviour is unchanged (the spawned pid is tracked directly — the default for a self-contained.exe). Each entry is a bare file name (no quotes, no path separators), matched case-insensitively; 1–16 names. The.exesuffix is optional — a native macOS process has none — but keep it on a card that travels: macOS matches with and without the suffix, sogame.exeworks on all three OSes, whereas a suffix-less name is only reliable on macOS (see macOS). Caveats:- anticheat / elevation — Steam / EAC / BattlEye often launch the game elevated or as a
service, which a non-elevated
tasklistcan't see (R4) → Playhook reports "didn't start" and quietly returns without recording a session. This is a common case for launcher games, not a rare edge. - generic names — don't use names like
game.exeor a sharedUnityPlayer-style binary: they can match an unrelated process. Use the game's specific shipping name. - already-running instance — don't open the game manually before pressing Play: presence matching would latch onto that pre-existing process.
- anticheat / elevation — Steam / EAC / BattlEye often launch the game elevated or as a
service, which a non-elevated
saveOnCardandpcSavePathare set together or both omitted. If both are omitted, the game writes its saves next to its exe on the card and syncing is fully disabled.backgroundMusic— card-relative likeheroImage, must lie inside the card root. It is off unless set (unless a global ambience is chosen in Settings), loops, and pauses while a game is running or the window is hidden; only global ambience in Settings makes the app's choice win over the card's. Use a common web-playable audio format (mp3, ogg/oga, opus, wav, m4a, aac, flac, webm). UI sounds are not a card setting — they always come from the navigation sound set chosen in Settings (asoundsblock in an oldergame.jsonis ignored, not rejected).killTimeoutSec— how long Force close (More ⋯ menu) waits for the game's processes to vanish before reporting a failure; the wait ends as soon as they're gone (optional, default 60).
Some games are too heavy to run from a micro SD / external drive (performance tanks), but the
installer — or the game's own folder, ready to be copied over — fits there fine. Add an optional
install block and the card carries a setup.exe (or the game directory) instead of running the game
from the card:
{
"schemaVersion": 1,
"id": "heavy-game",
"title": "Heavy Game",
"executable": "HeavyGame/HeavyGame.exe", // RELATIVE TO THE INSTALL DIR (not the card) in install mode
"install": {
"installer": "setup/HeavyGameSetup.exe", // installer path (for "copy": the game DIRECTORY), relative to the card root
"type": "nsis", // nsis | inno | custom | copy
"runAsAdmin": false, // run the installer elevated (optional, default false; forbidden for "copy")
"args": [] // type "custom": full argv with exactly one {dir} token (forbidden for "copy")
},
"launchTimeoutSec": 30
}How it works:
- While the game isn't installed there is no Play button: Install lives in the More (⋯) menu. Pressing it asks for confirmation (the popup also shows the destination path, handy if the installer isn't fully silent), then runs the installer silently and shows an "Installing..." indicator. When the executable appears Play comes back, and from then on the game launches from the install location on the PC.
type: "copy"— "move game to PC" without an installer.installerpoints at the game's own directory on the card and Playhook simply copies its contents into the install dir (executableis relative to that copy; a leading<source>/is stripped, so both spellings work). Nothing is deleted from the card — it stays the traveling original. This is the sane option when a game needs a fast local disk but has no installer worth driving, and the recommended one on the Steam Deck, where real installers under Proton are a lottery.args/runAsAdminare rejected here — no process runs.- The install location is controlled by the app, not the card:
%LOCALAPPDATA%\playhook\games\<id>(per-user, non-roaming, no admin needed). In install modeexecutableis resolved relative to that directory. - The installer runs silently because that's the only way the app keeps control of the install
path — a visible wizard would let the user change the destination. (If an installer refuses to behave
silently, tick Disable silent installer mode in Settings → General: the wizard is shown and you
point it at the destination path yourself — the confirmation popup spells it out for exactly this.)
The path is fed through each family's silent dir-key:
nsis→/S /D=<dir>(the/D=is unquoted and last, per NSIS rules);inno→/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /DIR="<dir>"— GOG offline installers are Inno Setup, so GOG games use"type": "inno";custom→ your ownargs, with the install dir substituted into the single{dir}token. You own the flags and quoting; the installer must support a silent + target-dir mode.
- The installer must honour the supplied directory. If it ignores it (or fails), the game's executable won't appear at the expected path → Playhook reports an error and stays on "Install".
type: "custom"withrunAsAdmin: trueis rejected:customlets the card control the argv, and running that elevated would escalate the attack surface. Fornsis/innothe app builds the args itself, so elevated is allowed there.- MSI is out of scope for now (its install-directory property name isn't standardized, so the path can't be controlled reliably).
- Once installed, the More (⋯) menu offers Uninstall instead of Install. It asks for
confirmation, then removes the game: Playhook finds the game's own uninstaller inside the install
folder (Inno's
unins000.exe, NSIS'sUninstall.exe) and runs it silently — so it cleans up the registry (Add/Remove Programs) and Start-Menu shortcuts — and then deletes%LOCALAPPDATA%\playhook\games\<id>. Acopy-mode game has no uninstaller: the folder is simply deleted. The menu item turns back into Install.
Getting type right matters: NSIS and Inno take different, mutually exclusive silent flags, and
they quote the target path differently (NSIS /D= is unquoted and last; Inno /DIR="..." is quoted),
so the wrong type breaks paths with spaces (e.g. %LOCALAPPDATA% for a user named John Doe).
- GOG games → always
inno. GOG offline installers (setup_<game>_*.exe) are built with Inno Setup. - File properties (right-click → Properties → Details): Inno often shows
This installation was built with Inno Setupin Comments; NSIS showsNullsoft Install System(or leaves the version fields blank). - Open it with 7-Zip: NSIS archives contain a
[NSIS].nsi/ NSIS script entries; Inno archives show Inno's structure ({app}, embedded payload). A detector like Detect It Easy (DiE) names the installer family outright. setup.exe /?: Inno pops up a dialog listing/SILENT,/VERYSILENT,/DIR=, etc. — which also confirms it supports the silent + target-dir mode Playhook needs.
The most reliable check is to dry-run the exact flags Playhook will use and confirm the game lands in the target folder with no wizard window:
# Inno (and GOG):
.\setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /DIR="C:\test\inno-check"
# NSIS:
.\setup.exe /S /D=C:\test\nsis-checkInstalled silently into the folder → that's your type. A wizard appeared, or it installed elsewhere →
it's the other type (or neither — use custom).
To reinstall or update, use Uninstall in the More (⋯) menu (or, as a fallback, delete
%LOCALAPPDATA%\playhook\games\<id> manually) and press Install again. Saves still sync to/from the
card exactly as for a normal game (the install block is orthogonal to saveOnCard/pcSavePath).
Cleaning up the registry and Start-Menu shortcuts is best-effort — it's done by the game's own
uninstaller, which Playhook finds in the install folder for typical NSIS/Inno games (with a registry
lookup as a fallback for a nonstandard NSIS uninstaller name). For type: "custom", or when the
uninstaller can't be found, only the install folder is removed and a registry/shortcut tail may remain.
Uninstalling targets the PC, so it's unaffected by the card being removed mid-operation.
Instead of carrying the game files, a card can be just a pointer to a Steam app by its numeric
appid. Add a steam block and Playhook launches, installs and uninstalls the game through your
local Steam client (via steam:// URIs) — the card only needs the manifest, the cover art, and
(optionally) the saves.
{
"schemaVersion": 1,
"id": "valheim",
"title": "Valheim",
"steam": { "appid": 892970 }, // the Steam application id (store URL / steamdb.info)
"watchProcesses": ["valheim.exe"], // REQUIRED in steam mode (see below)
"launchTimeoutSec": 120, // raise it — Steam cold-start / updates take time
"heroImage": "assets/hero.jpg", // optional, card-relative as usual
"saveOnCard": "saves", // optional save sync, exactly like a normal game
"pcSavePath": "%LOCALLOW%/IronGate/Valheim"
}There are no game files on the card — executable / install are not used (and are rejected if
present). heroImage, backgroundMusic, and saveOnCard / pcSavePath work exactly as
for a normal game.
Rules (enforced by the schema):
steam.appid— a positive integer. For a base gamerungameid == appid(read it off the store URL or steamdb.info). DLC / non-base launch ids are out of scope.watchProcessesis required.steam://rungameidreturns instantly and the game has no pid of its own, so the only way to track start/exit is by the game's process name(s) — same field and rules as for launcher games.executable,installandrunAsAdminare forbidden in steam mode (the launch method is exactly one: Steam).- Raise
launchTimeoutSec(e.g.120): a Steam cold start, shader pre-cache, or an auto-update before launch can easily exceed the default 30s window (see Known limitations).
How it works:
- "Installed" is Steam's own truth, read from the app manifest (
appmanifest_<appid>.acf) across every Steam library (libraryfolders.vdf); the Steam path comes from the registry. The game counts as installed only when Steam marks it fully installed (a game that's still downloading reads as "not installed"). - Not installed → no Play button; "Install" sits in the More (⋯) menu. Pressing it confirms ("Open
Steam to install this game?") and opens
steam://install/<appid>— Steam shows its own install dialog. Playhook does not block on a wizard: it stays on the screen, shows a non-blocking "Installing…" indicator, and a background poll (~5s) brings Play back once Steam reports the game fully installed. The window stays usable meanwhile — a Steam download can run for hours. - Pause: if you pause the download in Steam, the indicator becomes "Installing paused on N%…" (the percent is only available while paused — see limitations). While a download is in progress the activity button (the spinning gear) opens Steam's Downloads page so you can pause/resume there — Steam exposes no way to pause/resume a download programmatically.
- Launch: when installed, pressing Play opens
steam://rungameid/<appid>; the session is tracked bywatchProcesses(start → running → exit), with saves synced and stats recorded like a normal game. - Uninstall: an installed Steam game offers Uninstall in the More (⋯) menu → confirm →
steam://uninstall/<appid>(Steam's own removal UI). The background poll flips the item back to Install once the game is gone (an uninstall you trigger directly in Steam is picked up too). - Saves sync to/from the card exactly as for a normal game. Steam / Unity games often store saves
under
AppData\LocalLow— use the%LOCALLOW%prefix (e.g. Valheim →%LOCALLOW%/IronGate/Valheim).
If Steam isn't installed on the PC, Install/Play report "Steam is not installed" instead of opening
a URI. Steam's install and uninstall dialogs cannot be made silent — there is no steam:// flag to
suppress them (just one confirmation; the rest of the flow is automatic).
Statistics (hours / last played / launch count) are unified across machines with the card as the carrier:
stats.jsonin the card root is the traveling canonical record. It moves with the card.%APPDATA%\playhook\stats\<id>.jsonon each PC is a working mirror.
On every insertion the two are reconciled (field-wise: max of the cumulative totals, the
later lastPlayedAt) and the merged result is written back to both. Because the card is
physically a single device used sequentially, this never loses progress and never double-counts.
A fresh card with no stats.json simply adopts the local PC value (and starts carrying it from
then on).
Other PC state under %APPDATA%\playhook\:
pending-flush\<id>\— a deferred PC → SD sync with a snapshot of the saves, if the card was removed during play; it is applied on the next insertion of this card (matched byid).settings.json— the app's own settings, andlogs\— the log files.
On Linux the same state lives under ~/.config/playhook/.
Playhook runs on the Steam Deck (and desktop Linux/SteamOS). The same game.json cards work as on
Windows — the Windows dictionary (%APPDATA%, executable: *.exe, install.type: nsis|inno|copy) is kept,
and on Linux it is interpreted relative to each game's Proton (Wine) prefix. Windows games run
through umu-launcher + GE-Proton; native
Linux/ELF games are not a target.
- Download the
.AppImagefrom the Releases page, mark it executable (chmod +x Playhook.AppImage) and run it once in Desktop Mode. - In the tray menu, click Add to Steam. Playhook registers itself as a non-Steam game — tile, artwork and all — so it appears in your library and launches from Game Mode like any other title. The tile shows up the next time you enter Game Mode. (Adding it by hand via Steam's own "Add a Non-Steam Game" also works, but then Playhook cannot manage the entry: it will ask you to remove yours first.)
- First launch needs the internet: umu downloads GE-Proton and the Steam Runtime on the first run (cached afterwards). The status just shows "Running" while this happens — give it a minute.
Remove from Steam in the same menu takes the tile, its artwork and the auto-launch service back out.
python3 must be present (preinstalled on SteamOS; on desktop Linux install it if the app reports it's
missing — the bundled umu launcher is a Python zipapp).
Format the card exFAT. SteamOS mounts it (in Desktop Mode and in Game Mode alike), Windows reads
and writes it, and it is case-insensitive — so one physical card serves both machines: prepare it on
Windows and it works on the Deck with the same game.json. Playhook adds a safety net of its own: in
Game Mode it sweeps for an inserted-but-unmounted removable card and mounts it via udisks2 (into the
same /run/media/deck/<label> the system uses), so a card the session skipped still hot-swaps. Only
removable volumes carrying a real filesystem are ever touched — the internal drive can't match.
The card format itself is identical to Windows, with two caveats that come from Linux not sharing Windows' separator and (on Linux-native filesystems) being case-sensitive:
- Path case must match exactly on a case-sensitive filesystem:
"executable": "Game.EXE"won't findgame.exe. Playhook detects this specific case and the error suggests the right name ("foundgame.exe, fix the case"), but the card should carry the exact on-disk case anyway. On exFAT this never bites — it is case-insensitive. - Backslashes are fine.
"bin\\game.exe"is normalized tobin/game.exe, so a Windows-authored card loads unchanged.
- Per-game Proton prefix:
~/.config/playhook/prefixes/<id>— one Wine prefix per game (isolates runtimes, DLL overrides and saves). - Install mode puts the game inside that prefix (
…/drive_c/playhook/games/<id>) — i.e. on the Deck's internal drive, not the card. On a 64 GB Deck this adds up; keep an eye on it. - Saves are resolved inside the prefix:
%APPDATA%→…/drive_c/users/steamuser/AppData/Roaming, and likewise for%LOCALAPPDATA%/%LOCALLOW%/%USERPROFILE%/%DOCUMENTS%. For Steam-mode cards the save lives in that game's Steam compatdata prefix. The card still holds the traveling copy viasaveOnCard, so progress moves between machines as usual. - Uninstall clears the whole prefix for that game (game files, provisioned runtimes, and any in-prefix
saves), reclaiming the disk. Available for install-mode games and for a plain Executable game
once its prefix exists — there the game stays on the card and only the prefix is cleared. The canonical
save survives on the card when
saveOnCardis set.
These are ignored on Windows, so a dual-platform card can carry them safely:
winetricks(per game) andinstall.winetricks— extra winetricks verbs provisioned into the prefix before launch/install. Install mode always gets a baseline set (mfc42 gdiplus vcrun6 vcrun2008 riched20) so skinned Inno installers work; add more for a specific game (e.g.dotnet48, orvd=1920x1080to force a virtual desktop).umuGameId— a Steam appid or customumu-<id>so umu applies that game's protonfix.
- No tray in Game Mode. Playhook is a single window that always shows an empty "Insert a game card" screen when no card is present, and surfaces manifest errors on screen. Settings and Customize are screens of the launcher, reached from More ⋯, so both work here — keyboard and file browser included.
- Cards are mounted automatically — on top of what the session mounts itself, Playhook sweeps for an inserted-but-unmounted removable card (see Preparing a card for the Deck), so insert and eject just work.
- Power menu (Shutdown/Reboot/Sleep) works from Game Mode via
systemctl(logind). - The System submenu offers "Close Playhook" (a full quit) instead of "Minimize Playhook" — there is no tray to minimize into; quitting returns you to the Steam library.
- Insert a card and Playhook opens itself. Once you have used Add to Steam, a small background
service runs while you are in Game Mode and launches Playhook through Steam the moment you insert a
card carrying a
game.json— no need to find the tile. A card that was already in the slot when you entered Game Mode is deliberately ignored: you may well have come to play something else, and being yanked into Playhook on every entry would be obnoxious. Launch it from the library in that case. An ordinary USB stick or a card withoutgame.jsonnever triggers it. The whole behaviour can be turned off in Settings → General. - No autostart in Game Mode — the app lives as a non-Steam game, started by Steam (or by the service
above). In Desktop Mode it writes a
~/.config/autostart/playhook.desktopentry instead.
- Repacks with internal compression won't install. FitGirl/DODI/RG-style repacks whose installer
unpacks with its own 32-bit decompressor (ISDone/freearc) exhaust Wine's 32-bit address space and
crash mid-install — this is a Wine/32-bit ceiling, not a Playhook bug, and retrying is a lottery. Use a
clean distributive, or pre-extract the game on Windows and carry the ready folder — as a plain
Executable card, or with
install.type: "copy"if it should run from the Deck's internal drive. - Installers in general are unpredictable under Proton. Prefer a plain Executable card or
install.type: "copy"on the Deck. - First run needs network (GE-Proton / Steam Runtime download).
- Save-sync for Windows dictionary paths only happens once the game's prefix exists (first launch); before that there is simply nothing to sync.
Playhook runs on macOS (Apple Silicon only — there is no Intel or universal build). It is a narrower port than the Linux one on purpose: there is no Wine/CrossOver here, so macOS is about the games your Mac can actually run.
What works
- Local mac games — add them through More ⋯ → Add game → This PC and point Playhook at either a
plain executable or an
.appbundle (the picker shows a bundle as a single item, the way Finder does). Playhook resolves the real binary inside the bundle and tracks it by pid, so exit detection, playtime and save-sync all work as they do on Windows. - Steam mode — install, play and uninstall through your local Steam client, exactly as on the other two OSes.
- Cards carrying a
steamblock. Save-sync translates the card's Windows dictionary into the mac profile:%APPDATA%/%LOCALAPPDATA%/%LOCALLOW%→~/Library/Application Support,%USERPROFILE%→~,%DOCUMENTS%→~/Documents. - The tray icon, the power menu, autostart (System Settings → General → Login Items) and the whole UI.
What does not
- Windows games. A card whose
executableis a*.exerefuses to launch with a message saying so — running it would need Wine/CrossOver, which is out of scope. - Install mode (
install.type: nsis | inno | custom | copy) — the whole block is unavailable; those cards are rejected when the card is read. - Auto-update. Squirrel.Mac only updates a code-signed app bundle, and this build is unsigned, so Settings → Updates reports updates as unavailable. Download a new dmg by hand when one is released.
- The global Start+Back summon chord (it uses XInput, a Windows API). The gamepad itself works normally inside the launcher.
Things worth knowing
- Gatekeeper, for Playhook itself — see Download: first launch goes through System Settings → Privacy & Security → Open Anyway.
- Gatekeeper, for YOUR game — a game binary downloaded from the internet carries a quarantine flag,
and macOS kills it the moment Playhook starts it, with no dialog at all. Playhook catches that and says
so; the fix is the same Open Anyway (open the game once from Finder), or
xattr -dr com.apple.quarantine "/path/to/Game.app". Games installed by Steam are not affected. - Many Steam games have no mac build, or ship an x86_64 one that runs under Rosetta 2. When a game has
no macOS depot,
steam://installopens Steam and Steam refuses — Playhook simply stays on Install. watchProcessesnames. A native mac process is not called*.exe, so the field accepts a bare name too. Keep the*.exespelling on a card you also use on Windows or the Deck — Playhook matches with and without the suffix on macOS, so one spelling covers all three; a suffix-less name is for a mac-only entry. If a Steam game is not detected as running, its mac binary is simply named differently: fix it in More ⋯ → Customize → watchProcesses.- Save paths are a best-effort translation (see above). If a game keeps its saves somewhere else, the
sync reports the folder as missing rather than syncing the wrong one — point
pcSavePathat the real folder through Customize.
-
Windows 10/11 x64, Linux / SteamOS (for the Steam Deck build — see below), or macOS 13+ on Apple Silicon (for the mac build — see below).
-
Node.js 22.12+ and npm (required by electron 43 and @electron/rebuild 4; CI builds on Node 22).
-
Native module build tools — required to rebuild
drivelistfor Electron:- Visual Studio Build Tools with the "Desktop development with C++" component,
- Python 3.x in
PATH.
(
koffiships prebuilt, so the C++ toolchain is needed only fordrivelist.) -
Xbox gamepad (optional — keyboard and mouse work too).
-
A removable storage device (USB drive, SD card, etc.) with a prepared
game.jsonmanifest.
# from the project root
npm installnpm install runs electron-builder install-app-deps on its own via postinstall — this
rebuilds the native drivelist for the Electron version in use. If that step fails (no C++
toolchain), install without scripts and rebuild manually:
npm install --ignore-scripts
npm run rebuild # electron-rebuild -f -w drivelistCompile the TypeScript (main + preload + renderer) and copy assets, then run in dev mode:
npm run build # output goes to dist/ (main, preload, renderer, shared)
npm start # builds and launches Electron (always starts hidden in the tray)
npm run typecheck # tsc --noEmit (strict)
npm run lint # ESLint with type-aware rules
npm test # vitestnpm run dist # build + electron-builder → release/ (NSIS + portable)The configuration lives in electron-builder.yml. For drivelist,
asarUnpack is set so the native .node binary is available in the packaged build.
⚠️ Verify on a real packaged build thatdrivelistwas unpacked from asar and the card is detected. If not, make surenode_modules/drivelist/**is included inasarUnpack.
Build the .AppImage on Linux (or via the Build Linux CI
workflow — workflow_dispatch produces an artifact, a v* tag publishes to the release):
npm ci
npm run build:umu # fetch the bundled umu-launcher zipapp into resources/umu/ (pinned)
npm run dist # build + electron-builder → release/*.AppImagebuild:umu downloads the umu-launcher zipapp that ships as extraResources (Linux target only). The
linux block in electron-builder.yml targets AppImage; drivelist is rebuilt
for the Electron ABI by electron-builder install-app-deps on the Linux runner. koffi stays a
dependency but is never called on Linux (all FFI is behind lazy win32 guards).
Build the .dmg on an Apple Silicon Mac (or via the Build macOS CI
workflow — workflow_dispatch produces an artifact, a v* tag publishes to the release):
npm ci
npm run dist # build + electron-builder → release/*.dmg + *-mac.zipThere is no build:umu step (Proton is the Linux path). The mac block in
electron-builder.yml targets dmg + zip for arm64 only — a universal build
would also need a universal drivelist. The app is ad-hoc signed (identity: '-') and
hardenedRuntime is off: without the ad-hoc signature an app repacked on Apple Silicon refuses to launch
at all, and hardened runtime only pays off together with notarization, which needs a Developer ID this
project does not have. koffi stays in the bundle — gamepad-global.ts imports it on every OS.
⚠️ Build from a realnpm ciinstall. Ifnode_modulesis a symlink (e.g. a git worktree sharing the main clone's install), electron-builder cannot resolve transitive dependencies through it and silently packs an app that is missinggraceful-fs,js-yamland friends — it builds green and then dies on launch with a JavaScript error dialog.
Installed apps update themselves via electron-updater + GitHub Releases (public repo, so the
client needs no token). Only the NSIS build self-updates — the portable .exe does not.
Release flow:
- Bump
versioninpackage.json(e.g.0.1.1→0.1.2). - Commit, then push a matching tag:
git tag v0.1.2 && git push origin v0.1.2. - The Build Windows, Build Linux
and Build macOS workflows build and upload the Windows installer +
latest.yml, the Linux.AppImage+latest-linux.ymland the macOS.dmg/.zip+latest-mac.ymlto the same draft GitHub Releasev0.1.2. - Publish the draft release on GitHub to make it live (and visible on the Releases page).
- Each running app checks on startup and every 6h, downloads the update silently, and installs
it on the next quit (it never interrupts a running game). See
[updater]lines in the log. The NSIS build and the AppImage both self-update; the portable.exeand the dmg do not.
That is the default; Settings → Updates lets the user pick download and install automatically, download automatically, install manually, or off (check manually), opt into the pre-release (beta) channel, and run a check by hand.
Notes:
- The tag must be
v{version}and the version must be higher than the installed one. - The publish target (
owner/repo) is set inelectron-builder.yml— update it if the GitHub repo is renamed. - No code signing: the very first install shows a Windows SmartScreen warning, but updates still apply (unlike macOS, Windows auto-update works unsigned). That asymmetry is exactly why the macOS target ships without auto-update — Squirrel.Mac refuses to update an unsigned bundle, so the mac build reports updates as unavailable and the user re-downloads the dmg by hand.
On Windows and macOS the app registers itself via
app.setLoginItemSettings({ openAtLogin: true }) (on macOS it appears in System Settings → General →
Login Items).
- It always starts hidden in the tray (no flag needed): the window appears only when a valid game card is detected — unless Always show the no-card screen is enabled in Settings.
- Guaranteed for the NSIS installation; for portable it is best-effort (the path to the exe may change).
- To disable: Settings → Apps → Startup in Windows.
On Linux setLoginItemSettings doesn't exist. In Desktop Mode the app writes a
~/.config/autostart/playhook.desktop entry (Exec= = the AppImage path, best-effort). In Game Mode
Playhook lives as a non-Steam game you launch from the library — or that launches itself when you insert
a card, if you used Add to Steam (see Game Mode notes). That is a
systemctl --user service, playhook-daemon.service, bound to gamescope-session.target, so it only
exists while you are in Game Mode; Remove from Steam deletes it.
The main process writes a timestamped log, split per calendar day into
%APPDATA%\playhook\logs\main-YYYY-MM-DD.log (~/.config/playhook/logs/ on Linux,
~/Library/Application Support/playhook/logs/ on macOS); files older than
14 days are pruned on startup. Open the folder from Settings → Advanced → Open logs.
It records card insertions, manifest validation, the stats reconcile / card-copy result, and launch/exit — useful when a save or stats copy to the card silently fails.
- UAC / elevation: from a non-elevated app,
tasklistcannot see an elevated process. For an.exethat genuinely requires administrator, setrunAsAdmin: true— Playhook launches it through a UAC prompt and then monitors it by process handle instead oftasklist. What stays unsolved is elevation Playhook doesn't perform itself: a launcher/anticheat that spawns the game elevated or as a service is invisible → a false "didn't launch" timeout. - Launchers / wrappers: by default exit detection relies on the pid from
spawn, so a wrapper game (Steam / launcher) that immediately terminates its own process would produce a false "exit". This is now supported viawatchProcesses: list the game's own process image names and Playhook tracks those instead of the launcher pid. Caveat: if the launcher runs the game elevated or as a service (common with Steam / EAC / BattlEye), a non-elevatedtasklistcan't see it — the session won't be tracked. - Removing the card during play is handled gracefully (statistics on the PC, PC → SD deferred
to
pending-flush), but the game itself will most likely crash — the exe is on the card. The UI warns "do not remove the card during play". - FAT/exFAT: syncing goes "by direction";
mtimeis not used for decisions. - Install mode (the
installblock, see Install mode):- The installer must support silent install + a target-directory flag. If it doesn't, use
type: "custom"with the right flags — or give up on driving it and either tick Disable silent installer mode in Settings (you drive the wizard yourself) or carry the game withtype: "copy"(R-SILENT). - If the installer ignores the supplied directory and installs elsewhere, the executable won't
appear at the expected path → reported as "not installed". Inherent to running a real
setup.exe; doesn't happen with well-behaved NSIS/Inno installers (R-IGNOREDIR). - If an installer creates the executable early and then crashes, a presence check can't tell it apart from success. Mitigated by pre-cleaning the install dir and a post-exit grace poll, but a residual risk remains for arbitrary installers (R-PARTIAL).
- MSI installers are not supported (the install-directory property name isn't standardized).
- Reinstall/update means uninstall + install again — there is no in-place update: use
Uninstall in the More (⋯) menu, then Install (or delete
%LOCALAPPDATA%\playhook\games\<id>by hand as a fallback). - Progress is a plain "Installing..." indicator (no percentages — unavailable for an arbitrary
silent
setup.exe, and not reported for acopyeither).
- The installer must support silent install + a target-directory flag. If it doesn't, use
- Steam mode (the
steamblock, see Steam mode):- Steam must be installed on the PC, otherwise Install/Play report "Steam is not installed".
- Install/uninstall aren't silent — Steam always shows its own dialog (one confirmation); there is
no
steam://flag to suppress it. After that the flow is automatic. - No live download percent. Steam exposes no real-time progress in any file Playhook can read (the
.acfbyte counters freeze mid-download; the on-disk download folder is preallocated). The percent is shown only while the download is paused ("Installing paused on N%…"). - Can't pause/resume programmatically — the activity button just opens Steam's Downloads page so you can do it in Steam itself.
- Cold start / pre-launch update can exceed
launchTimeoutSec→ the game process never appears in the window and Playhook quietly returns without recording a session. RaiselaunchTimeoutSecfor Steam games.
src/
main/ # all work with the FS/processes/disks (Electron main)
platform/ # everything OS-specific (win32 / linux + Proton, umu / darwin)
preload/ # the typed contextBridge bridge
renderer/ # UI + gamepad/keyboard input (launcher, settings, customize — no Node)
shared/ # shared contract of types/IPC channels + the i18n dictionaries
test/ # vitest suites (plain Node, no electron)
npm scripts: typecheck, lint, test, format, build, start, rebuild, build:umu, dist.
Contributor conventions (layers, error handling, adding an IPC channel, the daemon's electron-free
import graph) live in CLAUDE.md.
PRs welcome. The codebase is strict TypeScript (no any, no non-null !, explicit return types,
functional style). Please run npm run typecheck, npm run lint and npm test before opening a PR —
CI runs all three, on Windows, Linux and macOS.
- Do I have to write
game.jsonmyself? No, twice over: the Playhook Collection hosts ready-made, verified manifests you can browse and drop straight onto a card, and the launcher's More ⋯ → Customize screen edits one through a form, with live validation. Hand-write one only for a game the Collection doesn't cover yet (and consider contributing it back). - Can one card hold several games? Yes — put an array of game objects in
game.jsonand switch between them in the history carousel (left/right, then A). - Is the SmartScreen warning normal? Yes. The builds aren't code-signed, so Windows warns on first run. Choose More info → Run anyway. Auto-update still works without signing.
- Does it run on the Steam Deck / Linux? Yes. The same Windows game cards launch through Proton (via umu-launcher) on SteamOS/Linux — see Steam Deck. Native Linux/ELF games are not a target.
- Does it run on macOS? Yes, on Apple Silicon — for what a Mac can actually run: native mac games
(including
.appbundles) and Steam mode. Windows.exegames and install mode do not work there, and the dmg does not self-update. See macOS. - Does it work without a gamepad? Yes — mouse and keyboard both work. Keyboard: WASD / arrow keys to move, Space / Enter to activate, Tab / Backspace (or Esc) to go back. Mouse: click a card in the row to select it and click it again to open it, the wheel flips through the row, and right-click goes back (the same as B). Holding left/right — on the gamepad or the keyboard — runs through the row quickly. Every gamepad action also has a mouse fallback (click Play, etc.).
- Can I use it with Steam games? Yes — via Steam mode:
a
game.jsonwith asteamblock launches, installs and uninstalls the game through your local Steam client (the card is just a pointer byappid). For a non-Steam wrapper/launcher.exe, point it at the exe and usewatchProcessesso exit detection works. - Is there a Russian UI? Yes — Settings → Appearance has English and Russian (plus a light/dark/system theme), and by default the app follows the system language.
"Find online" on the Add/Customize screen fetches a game's title, cover, backgrounds and music from external sources. Nothing is fetched without an explicit press, and everything applied is downloaded and stored next to the game, so a card keeps working offline.
- Titles, descriptions, covers and backgrounds: the Steam store.
- Wallpapers, offered first among the backgrounds: Wallhaven — no key needed, SFW only.
- More wallpapers, especially for recent releases: Wallpaper Cave — no key needed.
- Alternative covers: SteamGridDB — optional, needs your own API key.
- Backgrounds for games sold there: GOG.
- Soundtracks: Khinsider.
MIT — see LICENSE.
{ "schemaVersion": 1, "id": "hades", "title": "Hades", // The FULL path to the game on this PC. Only valid here — a card may never name an absolute path. "pc": { "executable": "C:\\Games\\Hades\\Hades.exe" }, "heroImage": ["assets/hades-hero.jpg"], "gridImage": "assets/hades-grid.jpg", // For a local game the save path may be absolute too (a card is limited to the %PREFIX% list). "pcSavePath": "C:\\Games\\Hades\\Saves", "watchProcesses": ["Hades.exe"] }