yuanzhengze 2f0e059b48 ps5upctl v0.1.0 — agent-first CLI for ps5upload
Stdlib-only Python CLI driving the ps5upload engine HTTP API, the console
helper, Payload Manager (:8084), elfldr (:9021) and FTP (:2121). Composite
doctor / repair / register / junk-clean commands encode a field-tested
troubleshooting methodology; 33 mock-server tests double as the API
contract spec. Field manual of real failure modes included.
2026-10-10 00:44:56 +08:00

ps5upctl — an agent-first CLI for ps5upload

ps5upctl is a single-file, stdlib-only Python CLI that drives everything ps5upload exposes on a jailbroken PS5, so that an AI agent (or a human who lives in a terminal) can diagnose and repair the console's homebrew state without clicking through a GUI:

  • the ps5upload engine HTTP API (loopback :19113 → console helper :9113/:9114)
  • Payload Manager (:8084, direct to console)
  • the ELF loader elfldr (:9021, raw socket with careful semantics)
  • the console FTP server (:2121, anonymous)

It was designed during — and field-tested on — a real troubleshooting session on FW 13.60 (ps5upload helper v5.41.0, ShadowMountPlus 1.7beta2, kstuff, Payload Manager 0.5.2), and its composite commands encode that session's hard-won methodology.

The field manual (why this tool exists)

These are the failure modes the CLI knows about, each with a hint in its error output and a doctor check:

  1. "No icon on the home screen" is a REGISTRATION problem, not a mount problem. kstuff/automount mounts .exfat images without registering them in app.db; the game is fully readable and still invisible. ps5upctl doctor flags not_registered; ps5upctl register <dir> fixes it.
  2. The stock elfldr wedges permanently if a client connects and closes without sending bytes (upstream ps5upload issues #344/#345, worked around in ps5upload's bundled third_party/elfldr). ps5upctl therefore NEVER bare-connects to :9021 — its health check sends the same harmless HTTP probe the upstream engine uses, and classifies the loader as healthy / stuck / absent.
  3. Half-written automounts. An image uploaded while kstuff is running can be automounted mid-upload; the mount point then lists as empty even though the image on disk is complete. ps5upctl repair = unmount (peeling stacked mounts) + clean remount.
  4. Payload Manager's OK lies. /loadpayload answers OK as soon as some loader accepted the connection; the payload may still not run (wedged elfldr). ps5upctl pm load PATH --expect PROCESS_NAME verifies the process actually appeared.
  5. The engine binds 127.0.0.1 by default; from another host you get 403 (opt in with PS5UPLOAD_ALLOW_IP on the engine host). The engine's saved console address also goes stale across DHCP changes — always pass --addr.
  6. 0x80940033 on launch = console on the lock screen, or a suspended game holding the big-app slot. Unlock the screen / kill the app, retry.
  7. FTP on :2121 doesn't implement NLST (502). Always LIST.

Install

No dependencies, Python 3.8+:

curl -O https://raw.githubusercontent.com/yuanzhengze/ps5upctl/main/ps5upctl
chmod +x ps5upctl
./ps5upctl --help

Environment: PS5_ENGINE (default http://127.0.0.1:19113), PS5_ADDR (default console IP), PS5_PM_PORT (8084), PS5_ELFLDR_PORT (9021), PS5_FTP_PORT (2121).

Run it on the same host as the ps5upload app/engine (the engine is loopback-only unless you opt it into LAN).

Quick start

# What's the state of everything? (non-destructive)
./ps5upctl --addr 192.168.2.36 doctor

# The "I uploaded a game and there's no icon" golden path
./ps5upctl volumes --addr 192.168.2.36          # find the mount
./ps5upctl register /data/homebrew/PPSA14501    # sce_sys pre-checked
./ps5upctl icon PPSA14501                       # PNG comes back -> registered

# A mount that lists as empty (half-written automount)
./ps5upctl repair --mount /data/homebrew/PPSA14501

# Upload residue eating disk
./ps5upctl junk scan
./ps5upctl junk clean --yes

# Cold console after a reboot
./ps5upctl doctor   # tells you the chain is down and what to re-run

Commands

Group Command What it does
Diagnose doctor Full non-destructive report: engine, PM, elfldr, FTP, every image mount (empty? registered? has sce_sys?), junk residue, findings + suggestions. --strict exits 1 when findings exist
version, status, focus, apps, appinfo TID, icon TID, volumes, ls PATH Read-only views over the engine/helper
syslog, klog, hw info/temps/power/storage/sensors, time, port-check IP PORT Console internals
Apps register DIR [--patch-drm-type] Register into app.db; pre-checks sce_sys/param.json and refuses cleanly if missing
unregister TID, launch TID ...
app kill/suspend/resume/list Lifecycle (find app_id in focus)
Mounts mount IMAGE [--at POINT] [--rw], unmount POINT, repair [--mount POINT] [--register] repair = unmount (loops for stacked mounts) + remount of source_image
Files delete PATH --yes, move SRC DST [--merge], junk scan/clean Destructive ops are confirmation-gated
Loader elfldr health Wedge-safe probe (never a bare connect)
elfldr ensure, elfldr send FILE --yes Engine-side patched loader / raw stream + half-close (EOF = execute), 64 MiB cap
PM pm version/payloads/processes, pm kill PID --yes Payload Manager direct
pm load PATH [--expect NAME] Load + verify the process actually appeared
pm upload FILE [--as NAME], pm delete NAME --yes Mind the filename namespace — PM derives its storage folder from the filename
FTP ftp ls PATH LIST-only (NLST is 502)

Connection flags (--engine --addr --pm-port --elfldr-port --ftp-port --timeout --text) work before or after the subcommand.

Output contract (for agents and scripts)

stdout is always exactly one JSON object:

{"ok": true,  "cmd": "doctor", "data": { ... }}
{"ok": false, "cmd": "register", "error": {"kind": "precheck_no_metadata",
                                           "message": "...", "hint": "..."}}

Exit codes: 0 success · 1 operation failed (pre-check, payload rejected, HTTP 4xx/5xx) · 2 transport/usage (engine/PM unreachable, bad args).

Error kind is stable and machine-branchable — the interesting ones:

kind meaning
engine_unreachable / engine_forbidden engine down / bound to loopback (403)
pm_unreachable, ftp_unreachable, elfldr_unreachable console-side service down
payload_rejected engine forwarded a helper error; detail.label + detail.cause, and a hint when the cause is a known token (register_metadata_missing, launch_sony_error_0x80940033, fs_unmount_not_our_mount, …)
precheck_no_metadata register refused before touching app.db (broken dump)
findings_present doctor --strict saw findings; full report in error.detail.report
needs_confirmation destructive op without --yes
pm_load_failed, payload_not_running PM accepted but the payload didn't run

--text switches to a compact human rendering (diagnostics still to stderr).

Safety model

  • Destructive ops are gated: delete, junk clean, pm kill, pm delete, elfldr send refuse without --yes.
  • The elfldr probe is safe by construction: it sends the upstream engine's harmless HTTP probe (a GET for a nonexistent file) and never a bare connect-and-close — the latter bricks the stock loader until reboot (issues #344/#345).
  • register pre-checks sce_sys/param.json before touching app.db.
  • repair never touches a mount whose title is currently running.

Tests

33 tests run the CLI against in-process fakes of every remote (engine, PM, elfldr healthy/stuck, FTP). They double as an executable spec of the API contract:

python3 tests/test_ps5upctl.py

Credits

  • ps5upload — the engine, helper and patched elfldr this tool drives.
  • Payload Manager — itsPLK. ShadowMountPlus — drakmor. kstuff and the loader ecosystem — the PS5 homebrew community.
  • The failure catalog above was gathered on FW 13.60 with ps5upload helper v5.41.0, PM 0.5.2, SMP 1.7beta2.

License

MIT (see LICENSE). This is an independent HTTP/socket client for the ps5upload engine API — it contains no ps5upload code. If upstream would like to adopt it into the main repository, relicensing to match is fine by us.

Languages
Python 99.9%
Makefile 0.1%