Command reference
Generated from the command registry. Every verb is one lib/porthole_cmd_<name>.py file exporting a SPEC dict.
| verb | what it does |
|---|---|
init |
set this host up: identity, address, build tier, pmaports, repo |
use |
switch the active device profile, and its working repo |
cd |
print a path to cd into: workdir, kernel, pmaports, profile |
next |
where am I in this port, and what is the one next thing |
brief |
everything an agent needs to start a session, in one call |
slots |
read A/B slot policy from the device, never guess it |
statusline |
render the build bar for an agent’s status line, or install it |
matrix |
what works on this device, tested separately from what exists |
permissions |
grant an agent the commands a bring-up runs all day |
doctor |
check the host, the profile and the device; name every fix |
pkg |
find, fork and build a userspace aport, with a real progress bar |
sandbox |
run pmbootstrap without handing the host to an agent |
verify |
every check that runs with no device attached |
tools |
search the toolbox and read a tool’s contract |
soc |
find devices sharing your SoC and inherit their working values |
dts |
write and check a device tree without starting from blank |
blobs |
get at vendor firmware during bring-up, without root |
config |
print the resolved config and where each value came from |
serial |
UART console: the channel that works before anything else does |
kconfig |
catch the kernel symbols olddefconfig silently dropped |
build |
build the kernel and package it, through envkernel |
flash |
flash the built boot image, honouring the slot policy |
log |
list, follow and rotate the build logs .run/ has been accumulating |
disk |
report the disk two divergent pmbootstrap work dirs are spending, and what is prunable |
devices |
list device profiles |
sync |
move the three repos between hosts: report, push, or fast-forward |
aports |
work on pmaports: status, feature branches, diffs, patches |
workspace |
every checkout on this desk: registered, worktree, shallow, dirty |
channel |
see and switch the postmarketOS release channel |
experiment |
run something with the device state captured either side |
ui |
see and switch the compositor / desktop |
push |
install a helper on the device where it survives a reboot |
brain |
search the second brain |
run |
run a tool with the config applied |
new-device |
scaffold a profile for a device nobody has ported yet |
completion |
emit a shell completion script (bash, zsh, fish) |
release |
plan images and validate release and hardware evidence locally |
docs |
generate the documentation site |
version |
version, environment and host tool versions |
init
Section: initSets this host up, and is safe to re-run on a half-configured one: it reads what is already there, offers it back as the default for every question, and rewrites only the lines you change.
Writes ~/.config/porthole/config.env – your username, your device’s address, your working repo, your tool paths. Never committed.
Interactive at a terminal, flag-driven otherwise, so an agent can bootstrap headless.
| argument | description |
|---|---|
codename |
device profile to use |
--user |
ssh username on the device |
--host |
device address (default: the USB gadget, 172.16.42.1) |
--port |
ssh port (default 22) |
--agent |
default TK_AGENT for the device mutex |
--workdir |
the device working repo (notes, logs, the kernel tree if you build one) |
--tier |
where builds run (default: workspace) (one of: workspace, host) |
--pmaports |
adopt this pmaports checkout |
--force |
overwrite an existing config |
--yes |
headless: actually write the config (interactive runs never need it) |
--non-interactive |
never prompt, even at a terminal |
--json |
machine-readable |
$ porthole init$ porthole init google-taimen --user user --host 172.16.42.1$ porthole init google-taimen --non-interactive # preview$ porthole init google-taimen --non-interactive --yes # write it$ porthole init google-taimen --workdir ~/ws/pmos/taimen$ porthole init google-taimen --tier host --pmaports ~/src/pmaportsuse
Section: useRewrites PORTHOLE_DEVICE in your config.env, so every tool, every shell and every later session agrees which phone you mean.
A profile that declares PORTHOLE_WORKDIR brings its repo along.
pmaports deliberately does NOT follow: it is one shared checkout, and
moving its branch behind your back is how a change lands in the wrong
package. use reports the branch instead.
| argument | description |
|---|---|
codename |
profile to switch to |
--workdir |
set this device’s working repo while switching |
--json |
machine-readable |
$ porthole use$ porthole use google-cheetah$ porthole use google-cheetah --workdir ~/src/cheetahPrints one bare path and nothing else, for command substitution:
cd "$(porthole cd)"cd "$(porthole cd pmaports)"Exits non-zero with an explanation on stderr if the path is unset or missing, so a shell function can fail loudly instead of cd-ing to ~.
| argument | description |
|---|---|
target |
workdir | kernel | pmaports | profile | porthole (default workdir) (one of: workdir, kernel, pmaports, profile, porthole) |
$ cd "$(porthole cd)"$ cd "$(porthole cd kernel)"$ cd "$(porthole cd pmaports)"next
Section: nextDerives the port’s state from what is actually on disk – the profile, the working repo, pmaports, the brain – and names the single next action, why it matters and the command for it.
A probe always outranks a checklist tick. Where they disagree it is reported as stale rather than believed: being told you are further along than you are is worse than being told nothing.
At a terminal it offers to run the next step when that step is safe. It never offers anything that flashes or touches the device.
| argument | description |
|---|---|
--json |
machine-readable |
--regenerate |
rebuild checklist.md from the milestone table, keeping ticks |
--run |
run the next step without asking (safe steps only) |
--yes |
regenerate: actually write the file |
$ porthole next$ porthole next --json$ porthole next --regenerate --yesbrief
Section: briefWhich device, is it reachable, what tools exist, what rules apply, what this device’s encoded traps are, and what to do next.
Read-only and safe to run at the start of every session. --json for
an agent; this is what AGENTS.md says to run first.
| argument | description |
|---|---|
--json |
machine-readable |
--no-device |
skip the device probe (faster, offline) |
--compact |
drop the findings and rule catalogues, which are lookups; keep every device fact |
$ porthole brief$ porthole brief --compact --json # for an agent: start here$ porthole brief --json # everything, as a reference$ porthole brief --no-device # offlineslots
Section: slotsSlot policy must be settled before anything is written to the phone, and the only honest source is the bootloader.
A value the device does not report is left UNSET and reported as unset. Guessing an active slot is how a port writes to the wrong one, and on a phone with no known-good image that is a brick.
| argument | description |
|---|---|
action |
probe (one of: probe) |
--yes |
write the probed values into the profile |
--json |
machine-readable |
$ porthole slots probe$ porthole slots probe --yes$ porthole slots probe --jsonstatusline
Section: statuslineA live build bar in the agent’s own UI, so nobody has to watch a
tool call. porthole build watch repaints with carriage returns,
and an agent running it inside a tool call sends that into a pipe –
the human gets a smeared mess or nothing at all. A repainting bar
has to be drawn by something the human’s terminal owns.
With no flags this reads Claude Code’s session JSON on stdin and
prints the line. --install wires it into a project’s
.claude/settings.local.json, ignored via .git/info/exclude so it is
never a tracked change in somebody’s kernel tree.
Only Claude Code can consume this today; OpenCode and Codex have it
as open feature requests. The portable half is already there for
them: .run/build-status.json and porthole build watch --json.
| argument | description |
|---|---|
--install |
wire it into a project’s .claude settings |
--project |
install into DIR (default: the cwd) |
--json |
machine-readable |
$ porthole statusline --install$ porthole statusline --install --project ~/src/taimenmatrix
Section: matrixAvailability and function are different questions and this asks both. A capability that is present and does not work reads as present-and-not-working, never as a tick.
Every cell prints the command that produced it, so a human can
re-run it and an agent cannot invent it. Where no probe exists the
cell is ? – which is not partial credit and never advances a
milestone.
Read-only and non-disruptive: every probe answers from state that already exists and induces nothing, so this is safe to run at any time. It never suspends the device or re-associates a radio.
| argument | description |
|---|---|
--json |
machine-readable |
--timeout |
seconds for the probe run (default 90) |
$ porthole matrix$ porthole matrix --jsonpermissions
Section: permissionsThe harness refuses commands it has not been told about, and on a port the same twenty recur all day – so a porter answers the same questions in every repo, forever.
The list is DERIVED, not typed: a tool is granted only when
is_risky says its name and summary name nothing irreversible.
Nothing that flashes, reboots, changes a slot or ramps a thermal
load is in it, and the preview says what was held back and why.
With no flags this is a preview and writes nothing. --install
merges into a project’s .claude/settings.local.json, keeping what is
already there, ignored via .git/info/exclude so it is never a tracked
change in somebody’s kernel tree.
It grants the device’s shell through tools/ph-device.sh, the wrapper
that takes the mutex. That is a judgement about a bring-up device and
the preview names it out loud; --no-device leaves it out.
| argument | description |
|---|---|
--install |
merge into a project’s .claude settings |
--project |
install into DIR (default: the cwd) |
--no-device |
do not grant the device’s shell |
--json |
machine-readable |
$ porthole permissions$ porthole permissions --install$ porthole permissions --install --project ~/src/taimendoctor
Section: doctorThe first thing to run on a new host, and the first thing to run when ‘all the tools are broken’. Every failure names a command that fixes it, chosen for your distribution where that matters.
Exits non-zero only on FAIL. Warnings are things you can work without.
| argument | description |
|---|---|
--json |
machine-readable |
--tools |
also check every tool is self-describing |
--bench |
also measure the performance budgets |
--all |
every check |
--no-device |
skip anything that touches the device |
--fix |
show the fixes, then offer to run them |
--dry-run |
–fix: print the plan and stop |
$ porthole doctor$ porthole doctor --no-device # host only, device unplugged$ porthole doctor --all --json # everything, for an agent$ porthole doctor --bench # measure, do not trust, the budgetspkg
Section: pkgporthole build is the kernel loop; every rung of it produces a
kernel artifact. This is the other half: a userspace aport, built
through pmbootstrap in the workspace, reporting through the same
tracker, status file and log that the kernel rungs already use.
The percentage here is REAL. ninja states its own total, so unlike a kernel build nothing has to be inferred from a previous run.
WATCHING ONE COSTS NOTHING. build --detach returns immediately and
the build outlives the session; watch follows it with a live bar in
any other terminal, and status --json is the one-shot an agent
reads instead of polling. Nobody has to sit on the output.
RESUME KEEPS THE TREE. pmbootstrap build runs abuild’s whole
sequence and deletes /home/pmos/build first, so “recompile three
files and repackage” costs a full build – 5.5 hours, for webkit.
resume runs abuild’s build/rootpkg actions against the tree that
is already there, under the same lock, tracker and bar.
TWO TREES, AND ONLY ONE OF THEM BUILDS. pmbootstrap keeps pmaports
and Alpine’s aports side by side, and pmbootstrap build reads
pmaports only – so Alpine’s twelve thousand packages are present,
useful, and unbuildable until fork copies one across. search
looks in both and says which tree a name is in, which is the actual
answer to “why does my build say the package does not exist”.
See docs/HANDOFF-package-builds.md.
| argument | description |
|---|---|
action |
build | install | resume | search | fork | status | watch | outdated | stop | owned | drift | rebase (one of: build, install, resume, search, fork, status, watch, outdated, stop, owned, drift, rebase) |
target |
build/fork: the aport. search: text to look for |
--arch |
build: target architecture (default: the profile’s) |
--timeout |
build: seconds before giving up (default 14400) |
--verbose |
build: stream the raw output |
--dry-run |
build: print the command and stop |
--detach |
build: start it in its own session and return |
--force |
build: rebuild even if the apk is current |
--pkgrel |
resume: set pkgrel in the aport AND in the build tree’s copy |
--apply-new-patches |
resume: copy the aport’s *.patch into the tree and apply them to src/ (abuild’s prepare is skipped) |
--actions |
resume: abuild functions to run (default: build rootpkg update_abuildrepo_index) |
--interval |
watch: seconds between reads (default 1) |
--wait |
build: queue this long for the buildroot instead of refusing |
--yes |
fork: actually write into pmaports. install: actually write to the device – without it, install only lists what it would put there. rebase: actually create the scratch worktree – without it, rebase only reports what the merge would do |
--tier |
owned: only this tier (default: all) (one of: required, optional) |
--fetch |
drift: git fetch the upstream aports clone first, so the comparison is current |
--json |
machine-readable |
$ porthole pkg search calculator$ porthole pkg fork gnome-calculator --yes$ porthole pkg build phoc$ porthole pkg build webkit2gtk-6.0 --detach$ porthole pkg resume webkit2gtk-6.0$ porthole pkg resume webkit2gtk-6.0 --apply-new-patches --pkgrel 53$ porthole pkg watch$ porthole pkg outdated$ porthole pkg status --json$ porthole pkg stop$ porthole pkg owned --tier required$ porthole pkg drift --json$ porthole pkg rebase mesa$ porthole pkg rebase mesa --yessandbox
Section: sandboxpmbootstrap needs root. The usual workaround – a multi-day sudo credential cache – gives every process running as you silent, unlimited root, which is not something to hand an agent.
So it runs in a persistent rootless container instead, where you are
root inside and your own unprivileged uid outside. No sudoers entry,
no standing privilege. up builds and starts it; shell --command
works without a TTY, which is what makes it usable by an agent.
build here means the CONTAINER IMAGE. To build a PACKAGE, the
verb is porthole pkg build <aport> – anyone reaching for
“build a package with porthole” lands on this verb first and
loses five minutes to it.
See docs/SANDBOX.md for the threat model.
| argument | description |
|---|---|
action |
status | shell | build (the container IMAGE, not a package) | up | down | gc (one of: status, shell, build, up, down, gc) |
--mount |
up: extra path to mount into the workspace |
--command |
shell: command instead of a shell |
--dry-run |
shell: print the podman command and stop |
--raw |
shell: allow a raw pmbootstrap build/checksum, bypassing the buildroot lock |
--force |
build: rebuild the container image even if the tag exists |
--broker |
install: the legacy sudoers broker, for a host with no podman |
--denied |
audit: only denials |
--limit |
audit: how many |
--keep |
gc: builds of each package to keep (default 2) |
--yes |
gc: actually delete; without it gc only reports |
--json |
machine-readable |
$ porthole sandbox up # build if needed, then start it$ porthole sandbox shell # a shell inside the workspace$ porthole sandbox shell --command pmbootstrap status$ porthole sandbox status$ porthole sandbox down$ porthole sandbox gc # what is superseded, deleting nothing$ porthole sandbox gc --yes # delete it, then reindexverify
Section: verifyA locked or absent device is the normal day-one state, so the offline gate is the only thing that works on day one.
A skipped check is NOT a pass: it exits 2, because a script that printed PASS while silently skipping the only check that reads reg properties is the kind of soft claim this whole toolkit exists to replace. Runs the port’s own verify.sh too — a device always has a check the toolkit cannot guess.
| argument | description |
|---|---|
--update-baseline |
record the warnings this tree emits today, so later runs fail only on new ones |
--allow-skips |
exit 0 even when a check could not run |
--json |
machine-readable |
$ porthole verify$ porthole verify --jsontools
Section: toolsNinety-odd tools is more than anyone can hold in their head. Filter by scope, by the device state they need, or by name; then read one’s contract without opening it.
For agents: porthole tools --json is the catalogue to consult first.
| argument | description |
|---|---|
name |
list | lint | audit, or a tool name to read its contract |
--scope |
generic | soc: |
--needs |
tools needing BOOTED, FASTBOOT, … |
--grep |
search names and summaries |
--core |
exclude the active profile’s tools |
--json |
machine-readable |
$ porthole tools$ porthole tools --needs BOOTED$ porthole tools --grep suspend$ porthole tools ph-suspend-cycle.sh$ porthole tools lint$ porthole tools auditsoc
Section: socThe highest-leverage question when starting a port: has someone already done this silicon? If so their deviceinfo holds boot image offsets that are known to boot, and their kernel tree holds a device tree you can read. Guessing those costs days; this costs 55ms.
| argument | description |
|---|---|
action |
show | list | inherit | diff (default show) (one of: show, list, inherit, diff) |
name |
show: a SoC (default yours). inherit/diff: a sibling codename |
--json |
machine-readable |
$ porthole soc$ porthole soc show qcom-sdm845$ porthole soc list$ porthole soc inherit oneplus-cheeseburger$ porthole soc diff xiaomi-sagitdts
Section: dtsA mainline device tree is layered: the SoC dtsi is already written, a board-family dtsi may cover most of the rest, and your .dts is often only a few hundred lines describing the board.
This finds the sources worth reading, scaffolds the file, lists the labels the SoC already defines, shows what a working sibling configures that you have not, and compiles what you wrote.
| argument | description |
|---|---|
action |
sources | new | labels | compare | check | port (one of: sources, new, labels, compare, check, port) |
reference |
compare: sibling DTS or codename. port: delta | apply | verify |
--from |
port delta: the OLD vendor tree |
--to |
port delta: the NEW vendor tree |
--origin |
port delta: where the vendor sources came from — without it the citations resolve to nothing |
--old-soc |
port delta: label for the old SoC |
--new-soc |
port delta: label for the new SoC |
--template |
port apply: the mainline sibling to rewrite |
--delta |
port apply/verify: the audit trail |
--against |
port verify: the mainline sibling to cover |
--out |
where to write the result |
--file |
operate on this file |
--include |
new: dtsi to include |
--author |
new: copyright line |
--year |
new: copyright year |
--force |
new: overwrite |
--dry-run |
new: print, do not write |
--json |
machine-readable |
$ porthole dts sources$ porthole dts new --dry-run$ porthole dts labels$ porthole dts compare xiaomi-sagit$ porthole dts check$ porthole dts port delta --from gs101.dtsi --to gs201.dtsi --out delta.toml$ porthole dts port apply --template gs101.dtsi --delta delta.toml --out gs201.dtsi$ porthole dts port verify --file gs201.dtsi --against gs101.dtsi --delta delta.tomlblobs
Section: blobspmOS packages firmware by fetching it in the aport. Before you can write that aport you have to know what is IN the vendor image – which blobs exist, which mainline driver consumes each one, where it expects them. That is a development problem and it comes first.
Sparse images are decoded directly (no simg2img) and ext4 images are read through debugfs, which does not mount and needs no privileges.
| argument | description |
|---|---|
action |
unsparse | unpack | ls | extract | inventory (one of: unsparse, unpack, ls, extract, inventory) |
path |
the image or archive |
--out |
output file or directory |
--dir |
directory inside the image (default /firmware) |
--dir-of |
inventory: classify an already-extracted dir |
--match |
extract: only these names |
--dry-run |
unpack: list what is inside, extract nothing |
--json |
machine-readable |
$ porthole blobs unpack factory.zip --dry-run$ porthole blobs unsparse vendor.img --out vendor.raw.img$ porthole blobs ls vendor.raw.img --dir /firmware$ porthole blobs extract vendor.raw.img --match 'wlan|bdwlan'$ porthole blobs inventory vendor.raw.imgconfig
Section: configEvery knob, its value, and which of the five layers supplied it. The provenance is the fastest way to answer ‘why is this talking to the wrong device’ – usually a stale config.env or an exported PHONE.
| argument | description |
|---|---|
key |
print just this key’s value, bare |
--source |
only keys that came from this layer |
--json |
machine-readable |
$ porthole config$ porthole config PHONE$ porthole config --source profile$ porthole config --jsonserial
Section: serialssh needs userspace, the USB gadget needs driver probe, the debug shell needs an initramfs. A UART needs none of them – it is the only thing that talks during early boot, and the only thing that says anything when a kernel dies before console handover.
The terminal is built in (termios, stdlib) rather than shelling out to picocom, because ‘install a terminal emulator first’ is a poor answer to ‘my device is printing something and I cannot see it’.
| argument | description |
|---|---|
action |
list | console | hardware (one of: list, console, hardware) |
--port |
console: which serial port |
--baud |
console: baud rate (default 115200) |
--log |
console: also append to a file |
--timeout |
console: detach after this long (for scripts) |
--all |
list: include built-in (non-USB) ports |
--json |
machine-readable |
$ porthole serial hardware$ porthole serial list$ porthole serial console$ porthole serial console --log boot.log --timeout 120kconfig
Section: kconfigpmbootstrap kconfig check validates a config against pmaports’
rules, and this does not duplicate it. It fills the gap that check
leaves:
olddefconfig resolves an unmet dependency by DROPPING the symbol and saying nothing. The defconfig is what you asked for; .config is what you got. The feature is then missing at runtime with no diagnostic, which reads as a broken driver rather than an unbuilt one.
| argument | description |
|---|---|
action |
diff | check | explain (one of: diff, check, explain) |
symbol |
explain: the symbol |
--asked |
the defconfig you edited |
--got |
the resolved .config |
--added |
diff: also show what the build turned on |
--all |
diff: include auto-detected toolchain symbols |
--watch |
diff: comma-separated symbols that MUST survive (default: PORTHOLE_KCONFIG_WATCH) |
--json |
machine-readable |
$ porthole kconfig diff$ porthole kconfig diff --asked a_defconfig --got .config$ porthole kconfig explain CONFIG_BINFMT_ELF$ porthole kconfig checkbuild
Section: buildThe envkernel loop, as a verb. It was only ever a shell file you had
to SOURCE, which meant porthole run could not reach it and the
second half of a port had no command at all.
Two traps are encoded in the script it drives, both paid for in real
sessions: pmbootstrap build --envkernel can write an apk and then
fail before refreshing the index, and a stale _p snapshot outranks a
release build. Artifacts are verified rather than exit codes trusted.
Every rung compiles a kernel tree except image, which builds the
whole system from pmaports as it stands and is what a host with a
pmaports checkout and no tree can run today. Builds go to the
workspace container when one is up; the preview says which.
With no ACTION, build shows the rung ladder – what each rung costs
and covers – and runs nothing: measuring is a real incremental make
and takes the buildroot lock, so the command you run to ask “what
would this do” must not itself be a build. Add --measure, or type
auto out, to actually time the make and route on what it rebuilt –
a module push if one module changed, a boot image if the dtbs did, a
full kernel if the tree moved under it. porthole build status says
what the last one did.
| argument | description |
|---|---|
action |
auto | status | watch | ccache | mod | boot | fast | kernel | deploy | upgrade | image | clean | purge (default: auto) (one of: auto, status, watch, ccache, mod, boot, fast, kernel, deploy, upgrade, image, clean, purge) |
rest |
mod: MODULE.ko NAME |
--kernel |
boot: rebuild Image.gz too, not just dtbs |
--timeout |
seconds before giving up (default 5400) |
--allow-env-override |
build what the environment says, not what the profile says |
--verbose |
stream the raw build output instead of a progress line |
--host |
run the build on THIS MACHINE instead of in the workspace container – needs pmbootstrap installed here, with its chroots |
--yes |
actually build |
--measure |
with no ACTION: time an incremental make and route on what it rebuilt, instead of just showing the ladder |
--detach |
start the build in its own session and return; follow it with build watch |
--interval |
watch: seconds between reads (default 1) |
--max |
ccache: raise the cache ceiling, e.g. 25G |
--wait |
queue for this long if the buildroot is busy (default: fail immediately) |
--json |
machine-readable |
$ porthole build$ porthole build mod drivers/media/i2c/imx179.ko imx179 --yes$ porthole build boot --yes$ porthole build boot --kernel --yes$ porthole build fast --yes$ porthole build fast --yes --detach$ porthole build watch$ porthole build watch --json$ porthole build kernel --yes$ porthole build image$ porthole build image --yes$ porthole build ccache$ porthole build ccache --max 25G$ porthole build cleanflash
Section: flashboot (default) flashes the boot partition only and leaves the
rootfs alone. full also replaces the rootfs and every
locally-installed package, and additionally needs –replace-rootfs.
The preview (no –yes) always renders, including while the device is in the wrong state – it names what is missing rather than refusing to say. Refuses to arm a slot the profile lists as forbidden: recovery from the bootloader cannot re-arm a slot, so this is the last point at which that mistake is cheap.
| argument | description |
|---|---|
action |
boot: boot partition only (default); full: rootfs and boot (one of: boot, full) |
--slot |
slot to arm (default PORTHOLE_ACTIVE_SLOT) |
--force |
flash even if the state probe disagrees |
--timeout |
seconds before giving up |
--yes |
actually flash |
--replace-rootfs |
confirm full may replace the rootfs and every locally-installed package |
--json |
machine-readable |
$ porthole flash$ porthole flash boot --yes$ porthole flash full --yes --replace-rootfs$ porthole flash boot --slot a --yeslog
Section: logEvery porthole build and porthole pkg run writes a full log to
.run/ and nothing ever read them back. porthole log lists them,
follows the one an in-progress build is writing, and – only with
–prune –yes – rotates the rest away.
| argument | description |
|---|---|
--last |
show only the N most recent logs |
--follow |
tail the log the running build is writing |
--rung |
only this rung’s logs |
--since |
only logs from within this long ago, e.g. 2d, 6h |
--prune |
delete logs past the keep window (destructive; needs –yes) |
--max-count |
how many logs –prune keeps (default 50) |
--max-age-days |
how old –prune lets a log get (default 14) |
--yes |
confirm –prune actually deletes files |
--json |
machine-readable |
$ porthole log$ porthole log --follow$ porthole log --rung fast --since 2d$ porthole log --prune --yesdisk
Section: diskporthole keeps two pmbootstrap work dirs – the host’s and the
workspace’s own – and neither is ever pruned automatically.
report (the default) shows their size, which apks are old
revisions, and where the two dirs hold different builds of the
same package. prune --yes deletes the old-revision apks;
retire-host --yes --discard-host-workdir removes the whole HOST
work dir (retire-host alone only previews).
| argument | description |
|---|---|
action |
report (default), prune old revisions, or retire-host (one of: report, prune, retire-host) |
--keep-revisions |
how many revisions per package to keep (default 2) |
--verbose |
print every divergent revision instead of a count and range |
--discard-host-workdir |
names the loss retire-host --yes causes: the whole host work dir, gone |
--yes |
confirm prune / retire-host |
--json |
machine-readable |
$ porthole disk$ porthole disk report --json$ porthole disk report --verbose$ porthole disk prune --yes$ porthole disk retire-host$ porthole disk retire-host --yes --discard-host-workdirdevices
Section: devicesEvery device profile in profiles/, and how complete it is.
| argument | description |
|---|---|
--json |
machine-readable |
$ porthole devices$ porthole devices --jsonsync
Section: syncThe device working repo, porthole, and pmaports – the three that have to agree for a build on the other PC to mean anything.
With no action it REPORTS: what branch each is on, what is uncommitted, and how far each is from its tracking branch. That is read-only and the one worth running before you start.
out pushes what is committed in each. in fetches and
fast-forwards each. Both STOP on a repo with uncommitted changes
and name the files, and in also stops on a repo that has
diverged. Nothing here invents a commit, rewrites history, resolves
a conflict or force-pushes – in is merge --ff-only and out
is a plain push, so git itself refuses anything else.
PORTHOLE_PMAPORTS_BRANCH, if set, is REPORTED beside the branch pmaports is actually on. It never changes the exit code: a feature branch there is the normal working state, and a check that fires on a healthy tree is one people learn to ignore.
See docs/DESIGN-fork-provenance-and-host-sync.md section 9.
| argument | description |
|---|---|
action |
status | out | in (default status) (one of: status, out, in) |
--yes |
out/in: actually write to the other repos |
--json |
machine-readable |
$ porthole sync$ porthole sync --json$ porthole sync out --yes$ porthole sync in --yesaports
Section: aportspmaports is a shared checkout that pmbootstrap also writes to, on a branch a channel switch will move under you. This answers what branch am I on, what have I changed, which of it is my device’s, and is it ready to send.
Everything here is read-only or an ordinary git operation. Nothing rewrites history and nothing pushes.
| argument | description |
|---|---|
action |
status | start | new | worktree | checksum | build | lint | ci | bump | patches | diff | patch (one of: status, start, diff, patch, new, checksum, build, lint, ci, bump, patches, worktree) |
name |
start: branch name. new: device codename. build/lint/checksum/bump: package |
--base |
branch/patch/patches base |
--mine |
diff: only your device’s packages |
--staged |
diff: staged changes |
--stat |
diff: summary only |
--out |
patch: output directory. worktree: where to put it |
--soc |
new: seed from the closest sibling on this SoC |
--category |
new: pmaports category (default testing) |
--kernel |
new: the kernel package to depend on |
--module |
new: initramfs module (repeatable) |
--changed |
checksum: every package with unstaged changes |
--arch |
build: target architecture |
--fast |
ci: fast scripts only |
--timeout |
build/ci: seconds before giving up |
--tree |
patches: the kernel checkout (default $PORTHOLE_WORKDIR) |
--pkg |
patches: the kernel package to write into |
--force |
new: overwrite. build: rebuild anyway |
--allow-dirty |
start: branch despite uncommitted changes |
--yes |
start/new/patches: actually do it |
--append |
patches: add these commits after the existing series instead of replacing it |
--drop |
patches: allow the rewrite to delete patches the tree does not reproduce |
--expect |
patches: assert –base resolves to exactly N commits; the only way past the size guard |
--json |
machine-readable |
$ porthole aports status$ porthole aports worktree --yes$ porthole aports start cheetah-gs201 --yes$ porthole aports new google-cheetah --soc google-gs201 --yes$ porthole aports checksum --changed$ porthole aports build device-google-cheetah --force$ porthole aports patches --base v6.18 --pkg linux-postmarketos-gs201 --yes$ porthole aports lint$ porthole aports diff --mine --stat$ porthole aports patchworkspace
Section: workspaceOne place to see what is actually on this desk. For each git checkout under the working repo: its branch, whether it is a linked WORKTREE or a clone, whether it is SHALLOW, how much is uncommitted, and which config key names it – or none, which makes it a stray.
READ-ONLY. It never deletes a checkout, never moves one, never fetches and never touches a working tree. Where there is something to do it prints the command and leaves it to you.
SHALLOW is the one worth acting on, and the reason the rest accumulates: a shallow tree cannot format-patch a series or rebase onto another base, so when a branch needs a different base a fresh clone becomes the only move – which is how a desk ends up with trees nobody chose to make.
A worktree is NOT sprawl. It is the cheap, correct way to have two
branches of one history, and it shares its parent’s object store –
which is why git remote get-url answers the same for both and why
two of them read as duplicated history when they are not.
| argument | description |
|---|---|
--root |
directory to inventory instead of the configured workdir (three levels) |
--json |
machine-readable |
$ porthole workspace$ porthole workspace --jsonchannel
Section: channelA channel selects a pmaports branch and an Alpine mirror. Switching means a rebuild, so this shows what the switch costs before you make it, and warns if pmaports has uncommitted work that a branch change would carry or clobber.
| argument | description |
|---|---|
name |
channel to switch to |
--yes |
actually switch |
--json |
machine-readable |
$ porthole channel$ porthole channel v26.06 --yesexperiment
Section: experimentMost wrong conclusions on a bring-up are confounds, not bad logic: a leftover process holding a device, a sound server holding a PCM, a wedged DSP from the previous run. The measurement succeeds and answers a question about a device you were not testing.
This snapshots the device before, REFUSES if it is already dirty, runs
the command, snapshots after, and diffs. Probes and confound checks
come from profiles/
| argument | description |
|---|---|
--tag |
name this run’s record |
--allow-dirty |
run even though a confound tripped |
--json |
machine-readable |
command |
probes to list what is captured, or the command to run, after – |
$ porthole experiment probes$ porthole experiment --tag mic-gain tools/ph-capture.sh 20$ porthole experiment tools/ph-suspend-cycle.sh 5pmOS ships 20-odd user interfaces. This lists the ones built for a handset by default, flags any that will not build for your architecture, and switches between them.
| argument | description |
|---|---|
name |
UI to switch to |
--all |
include desktop shells |
--yes |
actually switch |
--json |
machine-readable |
$ porthole ui$ porthole ui --all$ porthole ui plasma-mobile --yespush
Section: pushEverything scp’d to /tmp dies on reboot, and a bring-up session reboots constantly – a dozen times in one day, with every helper re-pushed each time.
This installs to /usr/local/bin, so the helper is on PATH and outlives
the reboot. Content is verified after it lands rather than trusting
the transfer’s exit code, and what porthole pushed is recorded so
push clear removes that and nothing else.
It does not survive a rootfs reflash. Anything that must belongs in the device package.
| argument | description |
|---|---|
files |
list, clear, or local files to install |
--yes |
clear: actually remove |
--json |
machine-readable |
$ porthole push tools/ph-sysstate.sh$ porthole push list$ porthole push clear --yesbrain
Section: brainScoped notes: laws, traps, playbooks, workflow. –scope filters to what applies to your device and always includes the generic notes, because hiding the laws from someone who filtered would be backwards.
| argument | description |
|---|---|
query |
search | new | lint | submit | reindex, then words to match or a note id |
--scope |
generic | soc: |
--subsystem |
filter by, or set on a new note |
--refutes |
new: for a finding – the theories it kills |
--severity |
law | trap | technique | fact – filter by, or set on a new note |
--json |
machine-readable |
--title |
new: the note’s title |
--confidence |
new: proven | probable | suspected |
--evidence |
new: what proves it |
--section |
new: laws | traps | playbooks | workflow |
--branch |
submit: branch name |
--message |
submit: commit subject |
--no-push |
submit: commit only |
--yes |
submit: actually do it |
$ porthole brain search --severity law$ porthole brain watchdog$ porthole brain search --scope soc:sdm845$ porthole brain new my-note-id --section traps$ porthole brain lint$ porthole brain reindexrun
Section: runOptional. Tools resolve config themselves, so tools/ph-fps.py works
directly. Use this to reach a profile-scoped tool by name, or with
–lock to take the device mutex with the right state declared.
| argument | description |
|---|---|
tool |
tool name, with or without extension |
--lock |
hold the device mutex, declaring the tool’s needs |
args |
arguments passed to the tool |
$ porthole run ph-fps.py$ porthole run --lock ph-suspend-cycle.sh 20$ porthole run ph-sysstate.sh # an on-device tool: piped over, not run herenew-device
Section: new-deviceCopies the documented template, seeds what can honestly be known from
fastboot getvar all and any existing pmaports deviceinfo, and writes
a checklist wired to the playbooks.
It does NOT invent a deviceinfo, defconfig or DTS. A confidently wrong one costs more than a blank: you end up debugging the device instead of the file.
| argument | description |
|---|---|
codename |
lowercase, dashes; e.g. oneplus-enchilada |
--from-fastboot |
seed from a device in the bootloader |
--soc |
seed from the closest sibling on this SoC, vendor-prefixed, e.g. qcom-sdm845 |
--force |
overwrite an existing profile |
--json |
machine-readable |
$ porthole new-device oneplus-enchilada$ porthole new-device fairphone-fp4 --from-fastboot$ porthole new-device google-cheetah --soc google-gs201 # new siliconcompletion
Section: completionGenerated from the live command registry, so a new verb gets completion for free and this can never drift out of sync.
| argument | description |
|---|---|
shell |
which shell (one of: bash, zsh, fish) |
$ porthole completion bash > ~/.local/share/bash-completion/completions/porthole$ porthole completion fish > ~/.config/fish/completions/porthole.fishrelease
Section: releaseNo device or external network access. Rehearsal may serve supplied assets on loopback. Writes only –output when supplied. Never publishes or signs. Verification checks artifact hashes; hardware claims require a separate report.
| argument | description |
|---|---|
action |
(one of: plan, inventory, catalog, verify, report, rehearse) |
--org |
directory containing local organization repository checkouts |
--pmaports |
package checkout used for planning and inventory |
--mirror |
local release asset tree to serve and test over HTTP |
--catalog |
directory of reviewed manifests/reports |
--manifest |
release manifest JSON |
--artifacts |
directory containing the manifest’s files |
--report |
reviewed hardware report JSON |
--output |
JSON file, or a Markdown directory for catalog |
--json |
machine-readable result |
$ porthole release plan --json$ porthole release catalog --output site-src/devicesdocs
Section: docsThe site is generated, never hand-maintained: the CLI reference from the command registry, the tool catalogue from the tool headers, the profile keys from the template, the knowledge base from brain/.
Every page has one source of truth, so a page that disagrees with the code is impossible rather than merely unlikely.
| argument | description |
|---|---|
action |
build | serve | new | lint (one of: build, serve, new, lint) |
name |
new: handoff | finding | session | status |
topic |
new: what the document is about |
--subsystem |
new: display, suspend, … |
--force |
new: overwrite |
--org |
GitHub org/user for links (default porthole-dev) |
--repo |
repository name (default porthole) |
--catalog |
reviewed release manifests and hardware reports directory |
--json |
machine-readable |
$ porthole docs build$ porthole docs serve$ porthole docs new handoff display-first-light --subsystem display$ porthole docs new status$ porthole docs lintversion
Section: versionWhat to paste into a bug report.
| argument | description |
|---|---|
--json |
machine-readable |
$ porthole version$ porthole version --json