Skip to content

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

Sets 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
Terminal window
$ 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/pmaports

Rewrites 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
Terminal window
$ porthole use
$ porthole use google-cheetah
$ porthole use google-cheetah --workdir ~/src/cheetah

Prints 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)
Terminal window
$ cd "$(porthole cd)"
$ cd "$(porthole cd kernel)"
$ cd "$(porthole cd pmaports)"

Derives 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
Terminal window
$ porthole next
$ porthole next --json
$ porthole next --regenerate --yes

Which 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
Terminal window
$ porthole brief
$ porthole brief --compact --json # for an agent: start here
$ porthole brief --json # everything, as a reference
$ porthole brief --no-device # offline

Slot 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
Terminal window
$ porthole slots probe
$ porthole slots probe --yes
$ porthole slots probe --json

A 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
Terminal window
$ porthole statusline --install
$ porthole statusline --install --project ~/src/taimen

Availability 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)
Terminal window
$ porthole matrix
$ porthole matrix --json

The 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
Terminal window
$ porthole permissions
$ porthole permissions --install
$ porthole permissions --install --project ~/src/taimen

The 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
Terminal window
$ 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 budgets

porthole 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
Terminal window
$ 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 --yes

pmbootstrap 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
Terminal window
$ 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 reindex

A 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
Terminal window
$ porthole verify
$ porthole verify --json

Ninety-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: | device:
--needs tools needing BOOTED, FASTBOOT, …
--grep search names and summaries
--core exclude the active profile’s tools
--json machine-readable
Terminal window
$ porthole tools
$ porthole tools --needs BOOTED
$ porthole tools --grep suspend
$ porthole tools ph-suspend-cycle.sh
$ porthole tools lint
$ porthole tools audit

The 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
Terminal window
$ porthole soc
$ porthole soc show qcom-sdm845
$ porthole soc list
$ porthole soc inherit oneplus-cheeseburger
$ porthole soc diff xiaomi-sagit

A 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
Terminal window
$ 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.toml

pmOS 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
Terminal window
$ 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.img

Every 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
Terminal window
$ porthole config
$ porthole config PHONE
$ porthole config --source profile
$ porthole config --json

ssh 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
Terminal window
$ porthole serial hardware
$ porthole serial list
$ porthole serial console
$ porthole serial console --log boot.log --timeout 120

pmbootstrap 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
Terminal window
$ porthole kconfig diff
$ porthole kconfig diff --asked a_defconfig --got .config
$ porthole kconfig explain CONFIG_BINFMT_ELF
$ porthole kconfig check

The 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
Terminal window
$ 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 clean

boot (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
Terminal window
$ porthole flash
$ porthole flash boot --yes
$ porthole flash full --yes --replace-rootfs
$ porthole flash boot --slot a --yes

Every 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
Terminal window
$ porthole log
$ porthole log --follow
$ porthole log --rung fast --since 2d
$ porthole log --prune --yes

porthole 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
Terminal window
$ 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-workdir

Every device profile in profiles/, and how complete it is.

argument description
--json machine-readable
Terminal window
$ porthole devices
$ porthole devices --json

The 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
Terminal window
$ porthole sync
$ porthole sync --json
$ porthole sync out --yes
$ porthole sync in --yes

pmaports 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
Terminal window
$ 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 patch

One 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
Terminal window
$ porthole workspace
$ porthole workspace --json

A 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
Terminal window
$ porthole channel
$ porthole channel v26.06 --yes

Most 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//probes.conf, because what counts as contamination is a device fact.

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 –
Terminal window
$ porthole experiment probes
$ porthole experiment --tag mic-gain tools/ph-capture.sh 20
$ porthole experiment tools/ph-suspend-cycle.sh 5

pmOS 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
Terminal window
$ porthole ui
$ porthole ui --all
$ porthole ui plasma-mobile --yes

Everything 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
Terminal window
$ porthole push tools/ph-sysstate.sh
$ porthole push list
$ porthole push clear --yes

Scoped 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: | device:
--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
Terminal window
$ 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 reindex

Optional. 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
Terminal window
$ 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 here

Copies 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
Terminal window
$ porthole new-device oneplus-enchilada
$ porthole new-device fairphone-fp4 --from-fastboot
$ porthole new-device google-cheetah --soc google-gs201 # new silicon

Generated 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)
Terminal window
$ porthole completion bash > ~/.local/share/bash-completion/completions/porthole
$ porthole completion fish > ~/.config/fish/completions/porthole.fish

No 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
Terminal window
$ porthole release plan --json
$ porthole release catalog --output site-src/devices

The 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
Terminal window
$ porthole docs build
$ porthole docs serve
$ porthole docs new handoff display-first-light --subsystem display
$ porthole docs new status
$ porthole docs lint

What to paste into a bug report.

argument description
--json machine-readable
Terminal window
$ porthole version
$ porthole version --json