TreeMap
무료 오픈소스 디스크 사용량 시각화 도구로, 사각형 트리맵·중복 파일 탐색·디스크 가득 예측·정리 시 파일을 휴지통으로만 이동한다
Watch, fix and control your SteamOS, Bazzite, CachyOS, Steam Deck or Windows gaming box from your phone. Free for 7 days, then $4.99 once, no subscription. Open-source service, LAN-only, no accounts.
생산성Python · TypeScriptGitHub @emerytech온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Monitor, fix, and play: your phone as the remote console + virtual gamepad for SteamOS/Bazzite HTPCs and Steam Decks. Open-source agent, source-available app.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Your phone is the dashboard, remote console, and game controller for your living-room Linux box.
Couchside pairs a native iOS & Android app with a tiny, dependency-free Python service that runs on a SteamOS / Bazzite HTPC, a Steam Deck, or any systemd Linux machine. When gamescope wedges into a black screen and the TV shows nothing, Couchside is the screen: see live vitals, read the logs, restart the display session, or become an Xbox 360 controller, all from the couch and entirely on your own LAN. No cloud, no accounts, no analytics.README 전문 보기 ↗Your phone is the dashboard, remote console, and game controller for your living-room Linux box.
Couchside pairs a native iOS & Android app with a tiny, dependency-free Python service that runs on a SteamOS / Bazzite HTPC, a Steam Deck, or any systemd Linux machine. When gamescope wedges into a black screen and the TV shows nothing, Couchside is the screen: see live vitals, read the logs, restart the display session, or become an Xbox 360 controller, all from the couch and entirely on your own LAN. No cloud, no accounts, no analytics.
The phone app and the service on your gaming box have separate version numbers. Build numbers identify a specific app package and are counted separately for each distribution.
| Component | Version | Build | Release information |
|---|---|---|---|
| iOS app | 2.9.78 | 261 | Store review and rollout status |
| Google Play app | 2.9.78 | 126 | Store review and rollout status |
| Android Direct app | 2.9.78 | 41 | Download APK |
| Linux service (gaming box) | 2.9.134 | — | Service release |
Store availability depends on review and rollout. See release notes for details. The latest signed service release is tagged v2.9.78; its title and agent-version.txt identify service version 2.9.134. The installers and updater use the stable latest-release URL. Historical releases remain available, and Direct downloads retain their stable couchside-direct tag.
Real footage — a phone driving Steam Big Picture on the TV over the LAN, nothing staged.
| Navigate & switch input modes | Browse and launch the library |
|---|---|
| Type from the phone | Big Picture & Quick Access Menu |
|---|---|
The full install — one command, scan the QR, drive the box — is on the site: couchside.tv (terminal sped up, nothing else). Every release since launch is written up at couchside.tv/updates.
The gamepad UI on the phone and the Steam sidebar responding on the TV, at the same time:
Plug a cheap nRF52840 board into the box, open Setup → Utilities, and tap
Flash receiver — Couchside writes the OpenPuck
firmware and the board reboots as a Steam Controller 2 wireless receiver. No cable
juggling, no cp to a mystery mount, no terminal. Below is real footage, end to end:
a bare nRF52840 hanging off a USB cable, the flash driven from the phone, and then
Steam's own controller list showing a Steam Controller Puck attached — the same
board, one tap later (the wait during flashing is sped up slightly).
OpenPuck is a separate AGPL-3.0 project by safijari,
flashed here from the EmeryTech fork. Couchside
does not embed or compile it: the box downloads the released firmware binary from the
fork at flash time and writes it to the board you plugged in. Because Couchside distributes
that binary, the corresponding source is offered at the exact shipped tag
(0.9.40) — see the app's
Setup → Open source licenses screen and agent/openpuck/OPENPUCK-NOTICE.txt.
/dev/uinput; games and Steam Big Picture see genuine wired-360 input. Four input modes, switchable with one tap:
Couchside can also drive a networked smart TV directly — no HDMI-CEC and no serial cable. Add one under Setup → Boxes → Smart TV remote: tap Scan for TVs and Couchside finds the sets on your network, so you don't have to hunt for an IP (you can still type one). Couchside identifies what is actually at an address before you try to pair with it, so aiming at the wrong device explains itself instead of just failing — and with more than one set on the network you choose which one the Pad drives, rather than getting whichever brand happens to rank highest. Once it's connected the Pad tab lights up with the D-pad, power, volume, the input/SOURCE key, and an on-screen keyboard — and your phone's own volume buttons drive the TV too.
These network backends run on the Linux service. The Windows service supports Roku (from 0.3.6-win); LG / Samsung / Google TV / Hisense are Linux-only for now. TV control also works through the box when it has an HDMI-CEC link or an RS-232 serial panel (power, volume, input source, on-screen remote) — see the volume/power control above.
agent/win/) — services, Event Log, Steam launching + Big Picture, volume, and a ViGEm virtual gamepad. Install it in one line from PowerShell: irm https://couchside.tv/install.ps1 | iex.On the box (or over SSH):
curl -fsSL https://couchside.tv/install.sh | bash
The installer copies the service to ~/.local/opt/couchside/, generates a token at /etc/couchside/token, installs a scoped sudoers rule, enables couchside.service, opens 8787/tcp in the local firewall, and finishes by printing a pairing QR code.
Steam Deck / SteamOS: a SteamOS update throws away every change in /etc that is not on its keep-list, so on SteamOS the installer also writes /etc/atomic-update.conf.d/couchside.conf, which lists only Couchside's own files there. If a box was installed before that and an update already took them, the app shows "Box installation is damaged". Re-running the installer command above in a terminal on the box (Desktop Mode on a Deck) puts them back. It keeps your existing pairing when the box still has its backup copy of the token, which Couchside 2.9.114 and later keep. An update started from the phone can't do this, because it has no password to write system files.
Decky Loader box? The installer also adds the Couchside Game Mode panel to Decky and lets it run the service. Don't want the panel? Remove Couchside in Decky's Settings → Plugins. The service keeps running, and the next update notices the panel is gone, remembers that (/var/lib/couchside/no-decky-panel) and runs couchside.service on its own from then on instead of putting the panel back. curl -fsSL https://couchside.tv/install.sh | bash -s -- --no-decky does the same up front; ... | bash -s -- --decky brings the panel back.
Running Bazzite? The same steps are in the official Bazzite documentation. On Windows, download CouchsideSetup.exe instead and double-click it.
Free 7-day trial with every feature unlocked, then a one-time unlock ($4.99 summer launch price). Prefer to build it yourself? The app's full source is right here: clone this repo and run it on your own device for personal use (see app/LICENSE).
Scan + PIN (recommended): on the Setup tab tap Scan for boxes. Couchside finds the boxes on your network, and the one you pick shows a PIN on its own screen — type that PIN into the app and it pairs itself. No IP, no port, no token to copy. The PIN appearing on the box's own display is the proof that you're the person sitting in front of it.
QR: point the phone camera at the QR code the installer prints. It's a couchside://setup?host=…&port=…&token=… deep link, so the app opens on the Setup tab with everything prefilled and runs a connection test automatically. Nothing is saved until you tap SAVE.
Manual (advanced): under Add by IP (advanced) enter the host (e.g. mybox.local), port (8787), and the contents of /etc/couchside/token, tap TEST, then SAVE.
Couchside separates local control, pairing and optional privileged operations:
Bearer token auth. Control APIs require Authorization: Bearer <token>; discovery and PIN pairing have separate gates. Native image reads and file uploads can instead use short-lived, scoped tickets. The gamepad WebSocket authenticates before the handshake completes. Comparisons are constant-time (hmac.compare_digest). The token file is chmod 600; on the phone it lives in the iOS Keychain / Android Keystore.
Scoped sudo, nothing more. The installer writes a visudo-validated sudoers file granting the service user passwordless sudo for a fixed set of exactly-argument commands — no wildcards, no globs, no directories. Six are always granted:
| grant | why |
|---|---|
systemctl reboot | |
systemctl poweroff | |
systemctl suspend | |
systemctl restart plugin_loader | revives Decky Loader's panel, which dies whenever Steam's CEF restarts and never comes back on its own |
systemctl restart --no-block couchside.service | lets an app-triggered update reload the new agent. Adds no privilege: that unit already runs the service user's own code as the service user |
| (root-owned journal wrapper) | see below |
Three more depend on what the box has. If a supported display manager is detected, the installer also grants systemctl restart <that manager> (the fix-a-black-screen rescue action) and a tee of one fixed file path, /etc/<manager>.conf.d/zzz-couchside-session.conf, which is how "boots into Game Mode / Desktop" is stored. On SDDM boxes a second tee, of the legacy zz- path, is granted so an updating box can blank its own stale pre-2.9.64 file. Both tee grants are root-owned file writes, and each names one exact path in the rule — this is not a grant of tee in general, and it cannot write anywhere else. A box with no supported manager gets none of the three: the agent degrades closed and the button never renders.
Note the journal grant carefully: it is for the wrapper, never for journalctl itself. The wrapper validates the unit and the line count and calls journalctl with a locked-down option set, because wildcard rule on would also permit / — turning "read the journal" into arbitrary file reads as root.
couchside command — enabling extra featuresThe installer adds a couchside command to manage the box. Most of it is
everyday (couchside update, couchside status, couchside pair), but four
subcommands are opt-ins that are OFF by default — they widen what a paired
phone is allowed to do, so you turn them on only on the box, never from the
app. Each prints exactly what it grants before it does anything.
| Command | What it lets the app do | Privilege it grants |
|---|---|---|
couchside allow-updates on | Trigger a Couchside agent update from the app | none new — runs the signed installer the box already trusts |
couchside allow-system-updates on | Update your box's Flatpak apps and OS (SteamOS/Bazzite) from the app | passwordless sudo for one fixed command each — a root-owned wrapper that runs flatpak update --system / stages an rpm-ostree/steamos-update, and accepts no other arguments. Never a grant on flatpak/rpm-ostree themselves. |
couchside allow-launchers on | Create custom launchers from the app (not just trigger ones you defined) | none new — a launcher runs as your desktop user, never root |
couchside allow-decky on | Install, repair or remove Decky Loader on the box and install Decky store plugins from the app | passwordless sudo for two exact systemctl start argvs (the pinned couchside-decky-loader@install / @uninstall oneshot units) plus the /etc/couchside/allow-decky marker. The root wrapper those units run gets no grant of its own. This one does install software as root — Decky Loader runs as root from your home by design — which is why it is its own switch and prints the full story first. See Security model. |
Turn any of them off again with ... off, or check state with ... status.
An OS update stages for the next boot — the app shows "reboot to apply"
rather than pretending it finished. Nothing here can run an arbitrary command as
root: each grant is on a single fixed wrapper or an exact argv, validated with
visudo, and sorted last (zz-couchside-updates, zz-couchside-decky) so a
distro's wheel rule can't shadow it. Only allow-decky can install new
software, and only the one thing it names. An interactive install offers the
Decky opt-in once (default no); a detached, app-driven update never asks and
never enables it.
Full detail on why the grants are shaped this way is in the Security model section above.
curl -fsSL https://couchside.tv/install.sh | bash -s -- --uninstall
That removes the service, sudoers rules, udev/modules-load drop-ins, the privileged helper and its units, the install dir (it asks before deleting the token), and the Decky Loader manager pieces if they were installed — the root wrapper /etc/couchside/couchside-decky-loader, the unit template [email protected], the allow-decky marker and declined stamp, the zz-couchside-decky grant, the run-state files under /run/couchside/decky-loader.*, and the user-side caches ~/.cache/couchside/decky-icons and ~/.cache/couchside/decky-job.json. Decky Loader itself and your plugins are left alone. To do it by hand:
sudo systemctl disable --now couchside.service
sudo rm -rf /etc/couchside /etc/sudoers.d/couchside ~/.local/opt/couchside
sudo rm -f /etc/udev/rules.d/99-couchside-uinput.rules \
/etc/modules-load.d/couchside-uinput.conf \
/etc/systemd/network/50-couchside-wol.link
sudo rm -f /etc/systemd/system/couchside.service && sudo systemctl daemon-reload
# SteamOS keep-list drop-in and the install manifest, if present
sudo rm -f /etc/atomic-update.conf.d/couchside.conf /var/lib/couchside/install-manifest
# Decky Loader manager pieces (agent 2.9.105+), if present; Decky itself is untouched
sudo rm -f /etc/sudoers.d/zz-couchside-decky \
/etc/systemd/system/[email protected] \
/run/couchside/decky-loader.lock /run/couchside/decky-loader.log \
/run/couchside/decky-loader.result
rm -rf ~/.cache/couchside/decky-icons ~/.cache/couchside/decky-job.json
sudo systemctl daemon-reload
Then delete the app from your phone.
Start with CONTRIBUTING.md for setup, focused test commands and review expectations. The code map shows the main components, request flow and security boundaries without depending on source line numbers.
agent/: Linux service, privileged helper and platform ports.app/: Expo / React Native phone app, native modules and tests.emerytech/couchside-decky repository.protocol/, tests/, .github/workflows/: shared contracts, regression checks and delivery.Use scripts/web-dev.sh for a local browser preview. Mock mode is useful for UI work, but does not replace device tests or isolate filesystem writes. The API reference is in agent/README.md.
The service is free and open source. The app's source is public. The official app builds are a free 7-day trial, then a one-time unlock: $4.99 summer launch price, rising to $7.99 on September 1.
app/): source-available under the PolyForm Noncommercial License 1.0.0. Read it, audit it, npx expo run:ios / npx expo run:android it onto your own phone, and modify it for personal, noncommercial use. What you can't do is sell it or redistribute it commercially. See app/LICENSE. The trial gate ships in this public source; self-built personal copies without a store build are treated as unlocked by design, and that's fine.curl to couch in under ten minutes. It's also the only funding this project has, so thank you.© 2026 Taylor Emery (ETS3D LLC).
couchside allow-decky on on the box, because it lets the phone manage a root-level service; the command spells out what that means before it does anything (see Security model).journalctl--file--directoryThe privileged actions are a fixed table; there is no route that runs an arbitrary command as root. Updating Flatpak apps or the OS from the app is an explicit opt-in (couchside allow-system-updates on, see the command table below) that adds a separate sudoers file granting one fixed root-owned wrapper each — never flatpak or rpm-ostree themselves, whose subcommands (install, run, override, rebase) would be arbitrary root. Off by default.
The privileged helper (agent 2.9.69+). Newer installs also place a small root process, couchside-helper.py, socket-activated by systemd. It exists to shrink the sudoers surface above, not to widen it: the same privileged operations are reached through a frozen verb table of eleven entries (session.set-boot, session.clear-boot, dm.restart, power, unit.restart, logs.journal, update.flatpak, update.os, — helper 1.1.0+ — decky.loader, which only starts the pinned Decky Loader unit described below and refuses without the opt-in marker, and — helper 1.2.0+ — usb.wake-arm, which writes enabled/disabled to a USB device's power/wakeup after validating the device id by membership in the kernel's own device listing, never interpolating it into the path, plus — helper 1.3.0+ — usb.wake-save, which persists that setting in a root-owned udev rule scoped to a validated physical USB port and device model, reloads the rules, verifies the live value and rolls back on failure), and an unknown verb runs nothing. It never sees a command, a path, or a shell string from a client — a verb selects a handler, and any argument is validated against a closed set before use.
Two independent auth layers, both fail-closed: the socket is 0660, owned by the install user's group, so nothing else can connect; and the helper re-checks the caller's uid with SO_PEERCRED and refuses anything that is not exactly the install user — including when the uid cannot be read at all. It listens on a local unix socket only and is not reachable from the network. It lives in root-owned /usr/local/libexec, never a user-writable directory, and couchside uninstall removes it along with both units.
Managing Decky Loader from the phone is a separate opt-in (agent 2.9.105+), and here is the thing to know before you turn it on. Decky Loader runs as root from your home directory. On a Decky box, anyone who can act as your desktop user — including a paired phone, through the gamepad — can become root. That is Decky's design, not something Couchside can wall off; it is true with or without Couchside, and it makes the standing systemctl restart plugin_loader grant above a root-exec primitive on such a box. Couchside refuses to make it worse, and says so plainly instead of hiding it.
Off by default. couchside allow-decky on (run on the box; it prints exactly this before asking) writes a separate sudoers file, zz-couchside-decky, granting two exact systemctl start argvs — [email protected] and [email protected] — plus a marker file, /etc/couchside/allow-decky, that the privileged helper, the unit's ConditionPathExists= and the root wrapper all check independently. The only new root logic is one fixed-procedure wrapper, /etc/couchside/couchside-decky-loader, which gets no grant of its own and can only be reached through those pinned oneshot units, so nothing can run it with other arguments. Step by step it: refuses if ~/homebrew or ~/.steam/steam is a symlink (root never follows a path under your home); downloads the latest stable PluginLoader from Decky's GitHub release over TLS with bounded, HTTPS-only curl; stages it beside the old copy in a root-owned services/ directory (never chmod 777, and nothing under your home is ever chowned to you); writes and pins Decky's own service file; restarts the loader and verifies it actually answers; and rolls back to the previous loader on any failure. Uninstall mirrors Decky's own uninstaller and keeps your plugins and settings. Decky publishes no checksum or signature for PluginLoader, so the download is protected by TLS and a host pin only — Couchside cannot verify what Decky does not sign, and does not pretend to.
Plugins are different: the bytes are handled entirely by Decky Loader itself — it downloads the zip from its own CDN, checks the SHA-256 the store publishes (Couchside always passes the hash so that check runs), extracts and starts it. The agent only talks to the loader over its loopback API, after confirming that what listens on port 1337 really is a root-owned Decky process. Plugins run as your desktop user, or — if a plugin is flagged for it in the store — as root; the confirmation on the phone says which. Decky's own in-app updater is not exposed (it un-pins the service file and follows whatever pre-release channel the box picked); the one loader-update path is Repair, which re-runs the same wrapper. couchside allow-decky off removes the grant and the marker; couchside uninstall removes all of it and leaves your Decky install alone.
Launchers run as you, and creating them remotely is opt-in. Beyond the fixed privileged actions, the app can trigger launchers — auto-discovered Steam games plus any custom commands the box owner defined. A launcher's command runs as the desktop user, never root. Because a custom launcher is an arbitrary command, creating one over the network is off by default: run couchside allow-launchers on to enable it, otherwise a bearer token can only trigger launchers you set up on the box, not mint new ones. (The same token can already synthesize keystrokes through the virtual gamepad, so treat the token as user-level trust on a machine you control — which is why it stays on your LAN.)
Journal access is allowlisted. Only units on the configured watchlist can be read, with line counts clamped server-side, so a leaked token can't be used to trawl the whole system journal.
Local control, optional internet integrations. Box control does not require a cloud relay. Updates, app-store purchases and optional integrations use internet services; the app is not entirely offline. Check the relevant integration before making privacy or network claims.
Transport protection. Paired clients use pinned TLS for supported API/WebSocket connections, normally on port 8788. The native file uploader in this source also streams over pinned HTTPS for secure pairings, without falling back to HTTP. This requires a new native app build; previously published apps still use scoped HTTP upload tickets. Legacy unencrypted pairings and native cover-art image loaders still use HTTP; scoped tickets limit authority but do not encrypt those payloads. Keep service ports on your trusted local network; do not port-forward them.
No client-addressable file routes. The service serves image bytes on four routes only: the album-art image a running media player advertises (realpath-allowlisted, image-sniffed, 2 MiB cap; client passes a player id, never a path); an on-demand screen-preview frame captured to a tmpfs file that's deleted right after (rate-clamped, off by default in the app); Steam cover art from the box's own library cache (client passes an app id); and the Decky store icon proxy (client passes a store id looked up in the box's own cached store list — the box fetches the icon from Decky's CDN with the host pinned and no redirects followed, caps it at 1 MiB, sniffs it as an image, and serves it from cache; the phone never talks to the CDN). Subprocesses run with shell=False, errors return brief JSON, never tracebacks.