Debug daemon availability
Use this runbook when commands report that the daemon is unreachable or unhealthy.
- Run
pohunek doctor --jsonfor local environment checks. On macOS this includesruntime_dir_private,socket_path_length,worker_executable,launchd_domain,launchd_job,filesystem_access(a Privacy & Security denial names the terminal app or the daemon executables to grant under Files and Folders; Full Disk Access is not the default remedy),login_shell,terminal,desktop_notificationsandkeychain; see local setup.daemon.doctorreturns the same list minus the CLI-onlylaunchd_joband current-directory probe, evaluated on the host that owns the runtime. - Run
pohunek health --jsonto query the local daemon. If path resolution fails first, inspectXDG_RUNTIME_DIR. It must be an absolute nonempty path when present. Linux requires it; macOS uses/private/tmp/pohunek-<effective-uid>only when the variable is absent and ignoresTMPDIRfor this decision. An overlong encoded daemon or worker socket path is rejected before filesystem mutation. If Pohunek reports an unsafe runtime root, inspect its owner, type, exact private mode, and every application-owned component. Do not recursively delete it or follow a symlink, and do not use broadchmodto make a foreign entry pass validation. The daemon also refuses to start whenHOMEor a forwardedXDG_*variable is one a worker job definition would reject: every value must be an absolute normalized UTF-8 path (no.or..segments), andHOME, the workers’ working directory, must exist. The fatal error names the variable; a relativeHOMEsuch as.fails this way even when everyXDG_*root is absolute. It also refuses to start when<data_dir>/metadata.jsonlhas a schema it cannot use: the fatal error names both schema versions. A store newer than the daemon needs the newer release (or themetadata.jsonl.pre-schema-<old>backup restored); see update after release. - If health cannot connect, run
pohunek service status --json. An installed service reports its daemon job (daemon.state,daemon.pid) anddaemon_errorwhen the service manager cannot be queried. On macOS a loaded job without a running process reportsunknownwithout apid, because launchd does not say whether it has not started yet or has exited; check the launchd output files below. Afailedorstoppeddaemon job is restarted by the service manager after its restart throttle; its logs are the daemon’s JSON log under~/.local/state/pohunek/logs/and, on macOS, the launchd output files under~/.local/state/pohunek/logs/launchd/. Ifinstalledis false, install the service withpohunek service install.pohunek daemon start --detachruns the installed daemon by hand and fails withservice_not_installedwhenservice.tomlis missing;pohunek daemon start --dev-subprocess --detachis for development only. - Run
pohunek health --jsonagain and inspect the reported socket, version, and status. - Run
pohunek host governance inspect local --jsonto distinguish a healthy daemon with a never-enrolled host from unavailable governance state. A never-enrolled result has four explicit null fields (enrollment,owner,owner_revision, andquarantine) plus a stable host ID and safe approval-key reference. Do not edit owner-private host-state files or keys to change this result. - For remote hosts, run
pohunek host inspect <host> --jsonand confirm the host daemon responds through the remote transport. If the remote daemon started before NetBird was ready, allow one retry interval for its NetBird-only listener to become available, then repeat the inspection and check its logs forserving control protocol over overlay. A daemon restart is not required for this startup ordering. If a listener supervisor exits or panics unexpectedly, the daemon logsremote listener supervisor failed; shutting down daemonwith the overlay identifier and exits after controlled cleanup instead of remaining ready without that listener. - If a session was expected, run
pohunek session list --jsonon the relevant host and inspect the specific session withpohunek session inspect <target>. - If the daemon restarted, do not infer session exit from the closed control or
attach socket. Check the session’s
runtime.state,worker_id, andworker_instance_id, then use the session runtime runbook forreconnecting,lost,conflict, orincompatible. - If
pohunek session neworsession forkfails withmigration_manifest_missing, the daemon found legacy resume bindings that no migration manifest imported and refuses to write a first session record that would make them unimportable. Follow the migration section of the session runtime runbook: runpohunek migration preflightthrough the worker-aware installer against the legacy daemon, and the next daemon start imports the manifest.
For durable notification issues:
- Run
pohunek notifications list --jsonto inspect visible local records. - Run
pohunek notifications list --status deleted --jsonwhen a record may have been logically deleted. - Run
pohunek notifications list --host <host> --jsonto query a specific remote daemon, orpohunek notifications list --all-hosts --jsonto compare local plus reachable hosts. - Run
pohunek notifications policy get --jsonand confirm the notification kind is enabled for the producer provider. Policy is enforced by the daemon for hooks, projectors, and any directnotification.createcaller. - Run
pohunek notifications policy set --provider default --kind turn_completed --enabled --jsononly when intentionally enabling noisy turn-completion records. A policy saved by an earlier release can still holdcodex,claude, andhermesoverrides that shadow the default flags; set each kind of such an override to the wanted value with the same command and--provider <runtime-id>. - Run
pohunek notifications retention prune --dry-run --status archived --jsonbefore applying retention cleanup. Use--applyonly after reviewing the dry-run ids. - Run
pohunek notifications watch --jsonin one terminal, then reproduce the event. A create should emitnotification_created; read, acknowledge, archive, provider upgrade, or dedupe upgrade should emitnotification_updated; delete should emitnotification_deleted. To follow one session, add--session <id>,--kind <kind>, or--status <status>(--unread); these filter the record in each create or update event client-side and drop delete events, so leave them off while checking that a delete is emitted. - If a provider approval prompt does not appear, reinstall hooks with
pohunek integration install --agent <codex-or-claude>and confirm the provider build supports the modern hook surface. Codex approval notifications require lifecyclePermissionRequesthooks; the legacy Codexnotifykey is not enough. Claude requiresNotification,Stop, andStopFailurehooks. Reinstall preserves user hooks unless their command exactly matches a Pohunek-managed hook command. First inspect every served runtime with daemon-managed hooks withpohunek integration status --json(Codex and Claude appear there only while their packages are installed), or select one withpohunek --host <name> integration status --agent <codex-or-claude> --json, which also reports hooks an earlier release left after the package is gone; daemon-backed status honors the effective host. For human output, remote recovery commands explicitly name the daemon host; run the local-only installer on that machine rather than adding--hosttointegration install. When a host profile points the agent at its own config home, the hooks must be installed there too: runpohunek integration install --agent <codex-or-claude> --profile <name>(or--all-profilesfor every distinct home) and check it withpohunek integration doctor --agent <codex-or-claude> --profile <name>.--all-profilesexits non-zero when any home failed and never rolls back across homes.currentverifies both executable assets with the installer-owned permission mode and effective-UID owner, exactly one managed registration under each expected provider event, and Codex feature/trust state. Asset type, UID, mode, and content come from one no-follow descriptor. The parent chain from the selected agent config root through the direct asset parent must contain only effective-UID-owned real directories with no group/world write access; ancestors above that explicit trust anchor are not inspected. Unsafe asset or parent ownership/permissions and duplicate managed registrations areoutdated. A missing Claudehooks/child under a trusted config root is a reinstallable absence; installation creates it with exact owner-private mode0700regardless of inherited umask, removes that newly created directory if mode enforcement or safe opening fails, and leaves an existing real user directory’s mode unchanged. Claudesettings.json, Codexhooks.json, and Codexconfig.tomlare each opened no-follow and require effective-UID ownership plus no group/world write bits; their metadata and bounded content come from the same descriptor. Codex managed trust uses a canonical single-handler group, so a sibling handler is drift even when the managed command itself is unchanged. The managed trust key set must also be exact. A trust record is installer-owned only when its trusted hash is that of a managed command for that event, so a stale managed record (a managed hash at an old position) is reinstallable drift and is removed during reinstall, while the records of a user’s own hooks in the same events are never removed or flagged and follow their hooks: a trust key embeds the group and handler index, so when install, reinstall, or uninstall shifts a user hook’s position, its record is re-keyed to the new position (its hash stays valid), and two claims on one key fail closed withconfiguration/integration_trust_conflictwithout writing anything (review the[hooks.state]tables by hand, then repeat the command). A scalar anywhere in the managed trust namespace requires configuration repair even when hook drift hides its current position.CLAUDE_CONFIG_DIRandCODEX_HOMEmust resolve to absolute UTF-8 paths, which keeps generated registrations independent of the daemon and provider working directories. Follow the typed recovery action:reinstallmeans the installer can repair every finding, whilerepair_configurationmeans provider files, symlinked, special, foreign-owned, or group/world-writable managed assets or parents, invalid registration roots, incompatible TOML table shapes, and scalar values anywhere in the installer-owned trust namespace must be inspected and fixed first. A missinghooksobject and an exact owned command with drifted handler metadata remain safely reinstallable. Oversized and non-regular files are rejected by bounded, nonblocking inspection and reported with the applicable recovery.pohunek integration doctor [--agent <codex-or-claude>] [--json]is the read-only diagnosis (it follows--host, like status, and exits non-zero when any finding is an error). It turns each status warning into a stable finding (asset_missing,asset_modified,asset_unsafe,registration_drift,provider_config_invalid,codex_hooks_feature_disabled,codex_trust_drift,config_root_invalid) with a remediation (asset_modifiedalso covers a script whose bytes differ from the embedded one at the same version marker, so an asset left over from an earlier release is an error even when its version did not change), reports a present agent without hooks ashooks_not_installedand an absent optional agent asagent_not_installed(both informational; only the absence expected of a not-installed integration is muted, so an empty config with an unsafe, symlinked, or group-writable directory is still an error, exactly where a later install would refuse), and checks what status cannot: whether the daemon’s runtime socket path, and the longest worker socket path, fit the platform limit (hook_socket_path_invalid), and an informationalpython3note. The daemon cannot know the agent’sPATHor working directory, so the note never fails the doctor or changes its exit code: it reports the first executablepython3behind an absolute entry of the daemon’s ownPATH(hook_runtime_python_found), or that none was found (hook_runtime_missing), or that it is the macOS stub with no Command Line Tools or Xcode behind it (hook_runtime_macos_shim, recognized through any alias of/usr/bin/python3). In every case the hooks runpython3from the agent’s ownPATH, so verify it there (on macOS checkxcode-select -pand install the Command Line Tools if they are missing). Relative and emptyPATHentries are skipped,python3is never run, and the note applies only to an agent whose hooks are installed.pohunek integration uninstall --agent <codex-or-claude> [--json](the agent is always named; there is no all-agents removal, so a failure can never leave one agent removed and another unreported) removes exactly what the installer owns, always on the local daemon host: the exact managed commands insettings.jsonorhooks.json, the Codex trust records inconfig.tomlwhose hash is that of a managed command (a user’s own hook records stay), and each managed hook script that is a regular file still carrying itsPOHUNEK_INTEGRATION_IDmarker line. The registration is edited first, so the agent never references a missing script. User hooks, other settings,[features] hooks, thehooks/directory, and the lock file are left as they are. A symlink, directory, FIFO, or unmarked file at a managed script path is preserved and listed underpreserved_paths; the marker is read from the same inode that is then deleted (it is moved aside first and moved back untouched when unmarked), and a provider file whose content, mode, or inode changed since it was read is a collision. Removal runs under the same lock and rollback as install, reports the sameintegration_install_in_progress,integration_destination_collision, andintegration_recovery_requirederrors, and is idempotent: with nothing installed (or no config directory) it reportsnot_installedand changes nothing. Status itself never repairs or rewrites provider configuration. Installation validates all existing config and hook parents before any mutation and fails unsafe parents withconfiguration/integration_path_untrusted. It performs replacements through already-opened directory descriptors with exclusive temporary files, explicit modes, and descriptor-relative rename, so a concurrent parent-name swap cannot redirect a managed write. Existing safe provider-file modes are preserved; new registration files use0600. An agent whose config directory does not exist is optional and simply not installed here: status reportsavailable: false,not_installed, recoverynone, and no warning. A config path that exists but is not a directory, or cannot be resolved or inspected, is a failure (outdated,repair_configuration) with a warning. A symlink at the config path, live or dangling, is such a failure (config_root_invalidin the doctor), never an absent agent, and install and uninstall refuse it. Status and doctor validate the config directory with the same read-only, descriptor-relative component walk the installer performs: a symlink at any component (live or dangling), a foreign owner, or a group/world-writable directory or ancestor isoutdatedwithrepair_configuration(config_root_invalid), for exactly the paths install and uninstall refuse. Replaced and removed originals are moved aside atomically into a private quarantine name bound to their inode and verified there (identity, mode, and content) against what was read, the new file is activated with a no-replace rename, and the originals are deleted only after every file is in place. A provider file that changed at any moment is a collision and is left intact, and so is a file the operation itself wrote that another writer changed before the operation finished: every written file is re-verified (inode, mode, complete content) before any script is removed and again before the operation completes and before any rollback, the other version is kept, the rest is rolled back without touching it, and the error names where the original stays quarantined; and so is a loaded input the operation leaves unchanged (for example an already-current Codexconfig.toml): it is verified again before any destructive step and right before the registration step; a failed step moves each original back untouched. If the final cleanup cannot delete a quarantined original, the install or removal still succeeds and lists it incleanup_incomplete; the doctor keeps reportingdisplaced_original_left_behindwith the paths until you review and delete them (or move one back if an install was interrupted). Every rollback or cleanup step that leaves data in quarantine (a removal refused because the entry changed, a restore that collided, a failed move) is reported with the true quarantine path: asintegration_recovery_requiredwhile the operation is still rolling back, or as acleanup_incompleteentry once it has committed. The doctor takes the installer lock without blocking and never creating the file, and holds it for its whole (short, read-only) inspection. While another install, uninstall, or doctor holds it, the doctor reads and scans nothing and reports only the informationaloperation_in_progressfinding for that agent (stillok, with no status); run it again afterwards. An installer that arrives during an inspection fails fast withintegration_install_in_progress(“another integration install, uninstall or doctor holds …”), which is safe to repeat. A lock file that is a symlink, a directory, has the wrong mode or owner, or was replaced makes every install and uninstall fail, so it is the error findingunsafe_installer_lock: delete it when nothing is running (it is recreated with mode 0600) or repair its permissions. If the createdhooks/directory cannot be removed or moved back after a failed install (the provider recreatedhooks/), the install reportsintegration_recovery_requiredwith the quarantine path that holds the directory and any files that appeared in it. The doctor recognizes the installer’s own quarantine names and every platform staging, parking, and stale prefix (including.pohunek-restore-*). Its scan stops after a fixed number of entries per directory, and an incomplete or failed scan is itself an error finding (quarantine_scan_incomplete), never a clean result: list the directory by hand for names starting with.pohunek-, review them, and remove unrelated files so the scan can finish. It finds these entries through the same no-follow trusted walk, only in a config root and Claudehooks/directory that passed validation, so a symlinked root orhooks/is never scanned; each directory scan and the listing are bounded, and the finding says when the scan stopped early. A rollback removes only the exact file the failed step activated (its identity is taken from the descriptor that wrote it before the rename), so a file another process put in place afterwards is kept and reported withintegration_recovery_required, which also names where the original is quarantined. Hints in these errors name the running operation, install or uninstall. Installation is one transaction over the ordered files (hook scripts first, provider registration last) under an exclusive lock file.pohunek-integration.lock(mode0600) inside the agent config directory, so installers for the same home or profile never interleave and installers for different profiles never contend. Failures are distinct and non-secret:runtime/integration_install_in_progressmeans another installer holds the lock (retry once it finishes);runtime/integration_destination_collisionmeans a provider file changed between the installer’s read and its write, and nothing was overwritten (rerun the install);configuration/integration_path_untrustedmeans an unsafe path shape (symlinked config root, directory, FIFO, wrong owner, or group/world-writable parent), and the foreign entry is left as it was;configuration/integration_home_ambiguous_ownermeans a request without--agentreached a config home that several runtimes reach through one handler (a package declaringclaude-hook-v1and Claude’s[config_home], say), so the home belongs to none of them and nothing was written to any home: name the runtime (pohunek integration install --agent <runtime>) or give the runtimes different config homes. Status reports such a home with that code and hint, the integration doctor as aconfig_root_invalidfinding whose remediation names--agent, andpohunek doctoras a warning, not as malformed state;runtime/integration_no_managed_runtimemeans a bareintegration installfound no served runtime with daemon-managed hooks (a clean host), and its hint names the official package install and thenintegration install --agent <runtime>;runtime/integration_update_incompatiblemeans the runtime’s hook schema does not admit the new asset set, and nothing was written (the descriptor and the installed release disagree, so update or reinstall the runtime package). Every file already committed is restored from a snapshot when a later step fails, including a displaced managed symlink, and ahooks/directory the install created is removed again when the rollback leaves it empty. When that rollback itself cannot restore a file, the error isruntime/integration_recovery_requiredand names each unrestored path: fix those files by hand, then rerunpohunek integration install. Managed hook scripts are fully owned by the installer, so a modified or oversized script is reinstall-repairable drift, while provider files are merged and never clobbered. A script too large to snapshot is moved aside under a.pohunek-integration-displaced-*name while the new one is written, so a failed install puts back the exact original inode, and an interrupted rollback leaves that quarantined original on disk and reportsintegration_recovery_required. Uninstall never deletes a script it cannot read and verify as owned; it lists it underpreserved_paths. On macOS the same checks apply. The installer never follows a symlink in the config root path, so aCLAUDE_CONFIG_DIRorCODEX_HOMEthat goes through a symlink (a dotfile-managed~/.claude, or a path under/var, which is a symlink to/private/var) fails withintegration_path_untrusted: point the variable at the canonical directory (realpath). Hooks connect to the socket path the daemon injected inPOHUNEK_SOCKET_PATH; that path is resolved and length-checked by the daemon (103 bytes on Darwin). Hooks stay silent by design, so when a macOS session reports nothing, first check that the daemon started with a short enoughXDG_RUNTIME_DIR(an overlong worker socket fails withworker_socket_path_invalid) before suspecting the hook. Parse errors in provider TOML are reported by position and message only, never by quoting the offending line. Status itself never repairs or rewrites provider configuration. Installation validates all existing config and hook parents before any mutation and fails unsafe parents withconfiguration/integration_path_untrusted. It performs replacements through already-opened directory descriptors with exclusive temporary files, explicit modes, and descriptor-relative rename, so a concurrent parent-name swap cannot redirect a managed write. Existing safe provider-file modes are preserved; new registration files use0600. - If a notification is missing its session link, inspect the hook environment
setup. The managed hooks silently drop an invalid
POHUNEK_SESSION_IDand still create the notification without linkage. - If duplicate attention notifications appear, compare each record’s
dedupe_key,source.provider,source.provider_event, andcreated_at. Session attention dedupe usesattention:<session_id>and only applies inside the policy’sattention_dedupe_window_secs. - If stale
agent_blocked,approval_required, orturn_completedrecords stayunreadafter the agent already resumed, check that the daemon still observes the session returning toworkingactivity. Session notifications auto-resolve toacknowledgedwhen the projector sees the transition intoworking, keyed byattention:<session_id>andturn:<session_id>, so hook- and projector-produced records are cleared together. A record that never self-resolves usually means noworkingactivity edge reached the projector (for example a session whose activity is not being reported). - If repeated
turn_completedrows appear for one session, inspect theirdedupe_key. Modern hooks sendturn:<session_id>forStop; a newer unread turn supersedes older unread turns for the same key, and a visible attention record supersedes the unread turn twin. Missingturn:keys mean the host likely needspohunek integration installso managed hooks refresh. - If an expected
agent_blocked,approval_required, orturn_completednotification does not (yet) show up inpohunek notifications list --jsonorpohunek notifications watch --json, it may simply be debounced: the daemon holds session creates pending in memory forattention_debounce_secs(5 seconds by default, seepohunek notifications policy get --json) before committing and emittingnotification_created. Wait past the configured window and re-check; if the session resolved back toworkinginside the window, the pending record was dropped and will never appear — that is the intended debounce behavior, not a bug. Pending debounced entries are in-memory only and are not persisted, so a daemon restart while an entry is pending drops that transient signal; this is expected for a sub-10s window and is not a data loss bug worth chasing.
The durable store is under the daemon data directory in notifications/.
notifications.jsonl is append-only record history, while policy.json is the
current persisted notification policy. Both are host-local; --all-hosts
commands perform client-side fan-out rather than reading a shared store.
Do not treat installed config files as the daemon source of truth. A missing
attach.conf or prompt template does not mean the daemon is unhealthy; verify
daemon health first, then run pohunek setup config for the missing files.