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.
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:
- "No icon on the home screen" is a REGISTRATION problem, not a mount
problem. kstuff/automount mounts
.exfatimages without registering them inapp.db; the game is fully readable and still invisible.ps5upctl doctorflagsnot_registered;ps5upctl register <dir>fixes it. - 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).ps5upctltherefore NEVER bare-connects to :9021 — its health check sends the same harmless HTTP probe the upstream engine uses, and classifies the loader ashealthy/stuck/absent. - 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. - Payload Manager's
OKlies./loadpayloadanswers OK as soon as some loader accepted the connection; the payload may still not run (wedged elfldr).ps5upctl pm load PATH --expect PROCESS_NAMEverifies the process actually appeared. - The engine binds 127.0.0.1 by default; from another host you get 403
(opt in with
PS5UPLOAD_ALLOW_IPon the engine host). The engine's saved console address also goes stale across DHCP changes — always pass--addr. 0x80940033on launch = console on the lock screen, or a suspended game holding the big-app slot. Unlock the screen / kill the app, retry.- 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 sendrefuse 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.jsonbefore 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.