Install pohunek manually
The README has a prompt that lets a
coding agent run these steps for you. This page is the manual path: release
archives, the login service, building from source, and a first session. The
setup guide covers configuration and pohunek doctor checks in depth; the macOS runbook
covers Homebrew, Gatekeeper, upgrade, and removal on a Mac.
Release archives
Section titled “Release archives”Each release publishes pohunek-cli-* and pohunek-daemon-* archives for
x86_64 Linux with both glibc and MUSL. Every
archive contains its license, the README, the reference pages under docs/
(features, install, CLI, SDK, development), and the offline knowledge
documentation under docs/offline/.
Daemon archives contain pohunekd, pohunek-sessiond, pohunek, and the
packaging/install-daemon.sh wrapper around pohunek service install, plus
runtime-catalog-anchor.json, the runtime-catalog trust anchor that the daemon
reads from the directory of its executable. CLI and relay archives do not
contain it. A release daemon archive also carries a runtime/ directory with
the signed runtime-catalog.json and, for the Linux x86_64 targets and the
Apple Silicon target, runtime/packages/<runtime>.tar.zst and
runtime/attestations/<runtime>-<target>.json. The MANIFEST covers these files.
Every CLI, daemon, and relay archive is packed deterministically
(members sorted, root-owned, stamped with the tagged commit time) and carries a
MANIFEST with the SHA-256 of every member; the daemon installer verifies
it, the host OS and architecture, and member permissions before they run or
change anything.
macOS on Apple Silicon (macOS 14 or newer) is not yet a published platform:
public macOS support is declared only when the final native acceptance gate
(#105) passes. Each release already publishes
pohunek-cli-<v>-aarch64-apple-darwin.tar.gz and
pohunek-daemon-<v>-aarch64-apple-darwin.tar.gz, each with a .sha256. They are
ad-hoc signed (the MANIFEST says signing adhoc), not notarized, and carry no
Developer ID. The daemon archive carries the signed runtime catalog and the
official runtime packages, as the Linux daemon archives do. A build made with packaging/macos/package --development is
unsigned, named ...-unsigned-development, and never released. The install,
upgrade, rollback, uninstall, log, logout/reboot, and Gatekeeper procedures are
in the macOS install runbook.
Every release asset (Linux, macOS, and SDK archives and tarballs, the runtime
package archives, runtime-catalog.json, the attestation documents,
release-inventory.sha256, and the .sha256 checksum files) has a GitHub
build-provenance attestation. release-inventory.sha256 lists every other asset
of the release in sha256sum -c format. A release becomes visible only after all
of them were uploaded and verified. Verify a download with:
gh attestation verify <file> --repo zajca/pohunekInstall on macOS with Homebrew:
brew install zajca/pohunek/pohunek # tap zajca/homebrew-pohunek, formula pohunekpohunek setupRun pohunek service upgrade after every brew upgrade pohunek, and
pohunek service uninstall before brew uninstall pohunek. The formula does not
touch launchd itself.
Or install from the archive: download the daemon archive with curl -fLO (curl
sets no quarantine attribute), check it with shasum -a 256 -c and
gh attestation verify, extract it, and run ./packaging/install-daemon.sh.
A file downloaded through a browser gets com.apple.quarantine, and Gatekeeper
blocks it because it is not notarized; see the runbook before removing the
attribute. After an upgrade macOS may ask again whether pohunek may access its
pohunek-relay Keychain items, because an ad-hoc signature has no stable
designated requirement.
On Linux, download from Releases,
unpack, and install the daemon archive with packaging/install-daemon.sh (see
below). If you copy the binaries onto your PATH by hand instead, copy
runtime-catalog-anchor.json next to pohunekd as well: the daemon reads the
anchor only from the directory of its own executable, and without it official
runtime packages are refused with official_trust_unavailable.
Protocol v2 was a one-time coordinated pre-1.0 boundary. Before that M1
transition, every CLI, SDK, custom client, and local or remote
daemon had to cross together. The legacy integer-v1 envelope and fixed
codex/claude notification-policy fields have no compatibility shim. Once a
fleet is on v2, peers negotiate their highest overlap: M2 and this M3 plugin do
not raise the public protocol version or require a second coordinated boundary.
Do not binary-downgrade a host after it has persisted Hermes enum values or the
provider-keyed notification policy; recover by upgrading forward instead.
For the daemon component, run the included packaging/install-daemon.sh. It
calls pohunek service install (or pohunek service upgrade when a service is
already installed and no interrupted install is pending), which:
- copies
pohunek,pohunekd,pohunek-sessiond, and the archive’sruntime-catalog-anchor.json(mode0644; a build without one installs without it, and an anchor that is not valid is refused withservice_staged_anchor_invalid) into<prefix>/libexec/pohunek/<version>/and installs<prefix>/bin/pohunek; a fresh install usesPOHUNEK_INSTALL_PREFIX(default~/.local), an upgrade keeps the prefix recorded inservice.tomland refuses a differentPOHUNEK_INSTALL_PREFIX; - writes
~/.config/pohunek/service.toml(mode0600) with every deadline, the agent environment allowlist, and the installation namespace; - registers the daemon as the only login service: the systemd user unit
pohunek-<ns>-daemon.serviceand slicepohunek-<ns>-sessions.sliceon Linux (systemd 255 or newer), or the launchd agent~/Library/LaunchAgents/io.github.zajca.pohunek.<ns>.daemon.pliston macOS; - waits until the daemon job’s own process answers on the socket, and journals
every step in
~/.local/state/pohunek/service-install.jsonso an interrupted run resumes or rolls back (an install that fails from the registration step on is kept pending for a rerun orpohunek service uninstallinstead).
The daemon starts each session worker as its own native job per worker
generation (a systemd transient unit, or a launchd job whose definition stays in
~/.local/state/pohunek/launchd/), so an upgrade restarts only the daemon while
live workers keep running from their versioned directory. Agents receive only
an allowlisted base environment ([environment] allowlist in service.toml)
rather than the daemon’s whole environment. pohunek service uninstall refuses
while sessions are live unless --stop-sessions is given, and keeps durable
metadata unless --purge is given.
pohunek service upgrade first runs a read-only preflight with the new
binaries. It judges every live session and every stopped session that keeps
native resume or fork, and refuses while one would lose its recovery or would
not be adopted by the new daemon (service_upgrade_sessions_at_risk, listing
each session with a reason code). The same preflight carries the release’s
official runtime plan: the packages the upgrade installs and the profiles it
pins. The upgrade installs and pins them in the same run, so a host on an older
release needs no manual package migration: one pohunek service upgrade moves
its built-in Claude Code and Codex profiles to the packages, adopts its live
sessions, leaves its stopped sessions resumable (a resume relaunches them from
the package digest), and reinstalls the hooks. A host already on official
packages gets the release’s versions: profiles pinned to an older version of the
same official package are repinned, and its stopped sessions move to the newer
version on their next resume or fork, see the update runbook. --accept-runtime-loss is only
for a session the new daemon cannot adopt, after you have reviewed the session
list; it never overrides a plan the release cannot apply
(service_upgrade_runtimes_refused, before anything is stopped) or a store the
new daemon would refuse to start with. See the
update runbook.
The first upgrade from a legacy daemon-owned PTY release refuses live sessions
by default because those open PTYs cannot be transferred. Let them finish; use
--accept-runtime-loss only after reviewing the affected ids and knowingly
accepting the destructive boundary. An install that still runs the older
pohunek-session@ template workers is retired only after the wrapper has
closed new connections to the legacy daemon, run the migration preflight, and
re-checked every not-inactive worker state after the daemon stopped; any
detected worker aborts the installer without removing legacy files. The whole
run holds the service transaction lock (pohunek service lock), and
pohunek service check first confirms that the final install or upgrade would
accept HOME, the XDG roots, the prefix, and every directory it writes. See the
migration guide and
operations runbook.
Or build from source (Rust 1.96+):
git clone https://github.com/zajca/pohunek.gitcd pohunekcargo build --release --locked \ --bin pohunek --bin pohunekd --bin pohunek-sessiondQuick start
Section titled “Quick start”# 1. Download the daemon archive for your platform from Releases, check it# with its .sha256 and `gh attestation verify` (see "Release archives"),# and extract it.cd pohunek-daemon-<version>-<target>
# 2. Set up the host. Run the pohunek of the extracted archive: it installs the# login service from this directory, the official runtime packages and# hooks for the agents on the service PATH, and the missing config files.# It previews every change and asks once; --yes applies unattended../pohunek setup
# 3. Start an agent session and attach to itpohunek session new --agent claude --name "fix-login-bug"pohunek attach <session-id> # Ctrl-] detaches, the agent keeps runningpohunek setup reports every requirement it cannot meet with one next step:
an agent that is not on the service PATH, a config home that does not exist
yet (Claude Code creates ~/.claude, Codex ~/.codex, on their first run), or
a platform whose release carries no package for a runtime. Run pohunek setup
again after you install an agent: it installs only what is missing, and on a
ready host it changes nothing. The CLI reference describes
each step, and the sections below cover the same steps one at a time
(pohunek service install, pohunek plugin install, pohunek integration install).
For an isolated feature branch, let the daemon create a dedicated worktree. To
run Codex there, install its package and then its hooks the same way;
--agent codex needs Codex’s config home (CODEX_HOME, else ~/.codex) to
exist, which Codex creates on its first run:
pohunek plugin install runtime/packages/codex.tar.zst --catalog runtime/runtime-catalog.json --yespohunek integration install --agent codexpohunek session new --agent codex \ --repo ~/Code/myapp --branch feat/retry-logic --base-branch main \ --input "Add retry logic to the API client, then run the tests."Install official runtime packages from the release archive
Section titled “Install official runtime packages from the release archive”Each Linux daemon archive carries the official runtime packages of its release and the signed catalog that authorizes them:
pohunek-daemon-<version>-<target>/ runtime-catalog-anchor.json trust anchor, installed beside pohunekd runtime/runtime-catalog.json signed catalog of this release runtime/packages/pi.tar.zst official Pi package runtime/packages/codex.tar.zst official Codex package runtime/packages/claude.tar.zst official Claude Code package runtime/packages/hermes.tar.zst official Hermes Agent package runtime/attestations/<runtime>-<target>.jsonThe same catalog and package archives are also published as separate release
assets (runtime-catalog.json, pohunek-runtime-<runtime>-<version>.tar.zst).
Every catalog entry pins its package to exactly the core version of its release
(core = "=<version>"). Current releases list the Linux x86_64 glibc and musl
targets and the Apple Silicon target.
-
Install or upgrade the daemon from this same archive with
./packaging/install-daemon.sh. A catalog from a release other than the running daemon’s is refused withpackage_incompatible, or withpackage_untrustedwhen it is older than a catalog this host has already accepted. -
Run
pohunek doctorand check thatcatalog_trust_anchorisok.warnmeans the daemon has no anchor (official_trust_unavailable),failmeans the anchor cannot be trusted (official_trust_anchor_invalid); see the runtime package recovery runbook. -
From the extracted archive directory, review what a package declares. The command prints the package id, version, digest, trust origin and the program and arguments the daemon will launch as you, then fails with
consent_required; nothing is installed:Terminal window cd pohunek-daemon-<version>-<target>pohunek plugin install runtime/packages/pi.tar.zst --catalog runtime/runtime-catalog.json -
Install it by repeating the command with
--yes, and check the result:Terminal window pohunek plugin install runtime/packages/pi.tar.zst --catalog runtime/runtime-catalog.json --yespohunek plugin listpohunek host inspect local --json
The CLI resolves both paths and the daemon on this machine reads them; the
command is local only and refuses a remote --host. The daemon verifies the
catalog signature against its anchor, refuses a catalog older than the newest
one it has accepted, and requires an entry that binds this package id, runtime
id, version and digest for this core version and platform. The package is
extracted into the owner-private package store and launches never read the
archive again; keep the archive and catalog (or the release assets): restoring a damaged
installation needs the same files, and an official archive can be restored only
while the daemon still runs the release whose catalog authorized it. The upstream agent itself is not part of the package: it
must be on the daemon’s PATH in the range the package declares (see the
Pi, Codex,
Claude Code and
Hermes package guides), or the
runtime reports itself unsupported.
codex, claude and hermes are reserved runtime ids that only a
catalog-authorized package may serve. The daemon compiles none of Codex, Claude
Code and Hermes in, so each is available only once its package is installed:
pohunek plugin install runtime/packages/claude.tar.zst --catalog runtime/runtime-catalog.json --yespohunek integration install --agent claudepohunek plugin install runtime/packages/codex.tar.zst --catalog runtime/runtime-catalog.json --yespohunek integration install --agent codexpohunek plugin install runtime/packages/hermes.tar.zst --catalog runtime/runtime-catalog.json --yesEach integration install installs that runtime’s hooks into its config home
(CLAUDE_CONFIG_DIR, else ~/.claude; CODEX_HOME, else ~/.codex; the
directory must exist); the package takes the native session id from them, so a
session without them cannot be resumed. A bare pohunek integration install
run before a package was installed did not cover its runtime. Until the package
is installed pohunek session new --agent claude (or codex) fails with
agent_profile_not_found, a host profile with base = "claude" (or codex)
and no package pin fails with runtime_not_installed, and pohunek integration install --agent claude (or codex) fails with runtime_not_installed; each
recovery hint names the matching install command. Hooks pohunek installed
earlier stay visible and removable without the package (pohunek integration status|uninstall --agent claude, or --agent codex). Without its package
pohunek session new --agent hermes fails with agent_profile_not_found and a
host profile with base = "hermes" and no package pin with
runtime_not_installed, with the same install hints. The Hermes operator plugin
lifecycle (pohunek integration ... --agent hermes) runs in the CLI against a
named Hermes profile; see the
Hermes operator guide. The catalog lists
the three packages for the Linux x86_64 targets and the Apple Silicon target.
A claude, codex or
hermes started in a shell session or outside pohunek is not recognized as that
agent without its package.
A session launched from the built-in Claude Code, Codex or Hermes of an earlier
release is not served by this daemon. Without the package every interaction with
it (input, attach, resize, rename, metadata) is refused too
(runtime_builtin_removed; only stop and remove work), so its work cannot be
finished until the package is installed. With the official package installed,
the package adopts the session, live or stopped, whatever its version, and
serves its input again. Resume and fork then run through the package, and the
relaunched run is pinned to the package digest. A session whose recovery the
package cannot honor (its native reference kind, a hook-reported reference, or
fork support for a fork), and a session record of v0.33.0 that names no package,
answer runtime_served_by_package, and its work continues in a new session. A
fork of a Hermes session answers agent_fork_unsupported, because Hermes has no
native fork. A host on an
earlier release that runs the built-in Claude Code or Codex needs no package
install before upgrading: the upgrade from a release with the official runtime
step installs and pins the packages itself, see the
update runbook. No earlier release archive carries the Hermes package, so finish or
stop built-in Hermes sessions before upgrading; the upgrade preflight reports a
live one at risk, and after an upgrade with --accept-runtime-loss installing
the Hermes package from this release’s archive lets you finish its work. On
a release without the upgrade’s runtime step, the first installed version of a
package is enabled, selected and takes the id over from that release’s built-in
runtime at once, and a host agent profile on that base must be pinned to the
package with pohunek plugin profile migrate <name> --yes (without --yes it
only previews the change); until then its launches fail with invalid_profile.
This manual migration is the fallback: the upgrade pins those profiles itself.
The same migration pins a profile over claude, codex or hermes on this
release once its package is installed.
The Hermes operator plugin comes only from the Hermes package as well: the CLI carries no copy of its payload, so install the package before the plugin:
pohunek plugin install runtime/packages/hermes.tar.zst --catalog runtime/runtime-catalog.json --yespohunek integration install --agent hermes --hermes-profile default \ --access-mode manage --allow-host localWithout the package, pohunek integration install|update --agent hermes fails
with hermes_package_not_installed and changes nothing; its recovery hint names
the package install. A plugin an earlier CLI installed from its compiled copy
(provenance source=embedded) is not migrated automatically: status and
doctor report the missing package, integration uninstall still removes it,
and integration update moves it to the package once the package is
installed.
To prepare first, install with --no-enable and run
pohunek plugin enable <package-id> when you are ready to switch. pohunek plugin disable or pohunek plugin uninstall leaves claude, codex or
hermes unserved. The upgrade installs and selects the release’s package for
every official runtime the host uses. It repins the host profiles pinned to an
older version of that package and lists the pinned profiles it keeps. Outside
an upgrade, pohunek plugin update <package> runtime/packages/<runtime>.tar.zst --catalog runtime/runtime-catalog.json installs and selects a newer release’s
version. It refuses a version lower than the selected official one
(plugin_update_downgrade), and a version that cannot be ordered against it
(plugin_update_version_unordered); pohunek plugin select rolls back on
purpose. When
the selected version is newer than the release’s, for example after a
plugin update from a later archive, the upgrade plan says that the upgrade
re-selects the release’s version. Select the newer one again afterwards. A plain
install of another version leaves it installed but unselected.
The trust model and the package classes are described
in the architecture and the
runtime packages guide.
Install the Pi runtime package from a checkout
Section titled “Install the Pi runtime package from a checkout”Pi (pi, the Pi coding agent) is an optional runtime delivered as a runtime
package, not compiled into the daemon. The package declares launch, native
resume and fork, input framing, and detection for Pi 1.x from 1.0.0 (verified
on 1.0.2); the supported range and the verified release are in
compat/pi/compatibility-lock.json. It needs a
Pi on the daemon’s PATH (npm install --global @earendil-works/pi-coding-agent@1.0.2, Node 22.19 or newer). A release install
takes the official package from the daemon archive as above; a checkout builds
the archive and trusts it by its digest instead.
# From a checkout: build the archive and note the digest it prints.cargo xtask package build runtime-packages/pi --output pi.tar.zst
# Review what it declares, then install it with that exact digest (local trust,# never official). Repeat with --yes to install.pohunek plugin install ./pi.tar.zst --sha256 sha256:<digest-printed-above>pohunek plugin install ./pi.tar.zst --sha256 sha256:<digest-printed-above> --yes
pohunek host inspect local --json # the pi runtime reports its version and `supported`pohunek session new --agent pi --name "refactor-parser" --input "Refactor the parser."- Pi chooses its own conversation file from the session id the daemon passes
at launch (
pi --session-id <id>), sopohunek session resumerunspi --session <id>andpohunek session forkrunspi --fork <id> --session-id <fork id>under a second id the daemon generates for the fork. - Pi writes its session file only after the first model reply. A session that
never got one has nothing to resume, and resume is refused with
agent_native_reference_missingbefore anything launches. The same code answers a session file that was deleted. - A forked session holds its own reference from the start: Pi writes the
fork’s session file
<time>_<fork id>.jsonlat once, before any model reply, sopohunek session resumereopens the fork withpi --session <fork id>. Forks get their own reference from Pi package 1.0.1 on; a session launched from package 1.0.0 keeps forks without one. - Recovery looks for the session file below
PI_CODING_AGENT_DIR(default~/.pi/agent) insessions/, or directly inPI_CODING_AGENT_SESSION_DIRwhen the profile[env](or the daemon’s environment) sets it to an absolute path. From Pi package 1.0.2 on, a launch whose Pi would keep its sessions elsewhere (--session-dirin the profileargs, or asessionDirsetting while that variable is unset) is refused withagent_native_store_relocated; setPI_CODING_AGENT_SESSION_DIRto that directory instead. - A Pi extension dialog, the project trust prompt and the login steps read as
blocked; the model, session and other selectors are never read asidle. - A Pi outside the supported range refuses to launch with
agent_runtime_unsupported, whose message names the detected release and the range; install a Pi in the range, or update the package (pohunek plugin update) when a package with a new range is released.