Hook binary distribution
The aiot hook binary is distributed via GitHub Releases. Each release includes eight platform-specific binaries (a launcher + a runtime for each of four targets) and a SHA256SUMS-hook checksum file.
The split into a launcher and a runtime exists so that macOS Background Task Management (BTM) attributes background processes to our signature rather than Bun’s. Without the launcher, Activity Monitor’s “App Background Activity” shows “Jarred Sumner” (Bun’s author) because BTM reads the code-signing identity of the executable backing each LaunchAgent. The launcher is a ~300 KB Rust binary that execvs the ~50–80 MB Bun-compiled runtime next to it; the user never interacts with the runtime directly.
| Binary | Size | Installed as | Purpose |
|---|---|---|---|
aiot-<target> |
~300 KB | ~/.local/bin/aiot |
Rust launcher — execs the runtime |
aiot-runtime-<target> |
~50–80 MB | ~/.local/bin/aiot-runtime |
Bun-compiled CLI (all commands) |
Installation is a two-step process handled by two separate installers:
| Step | Installer | What it does |
|---|---|---|
| 1. Binary acquisition | scripts/install.sh (shell script) |
Downloads both binaries, verifies checksums, and places them on your PATH |
| 2. Delivery setup | aiot install (CLI subcommand) |
Auto-wires hooks into detected agent harnesses and sets up how delivery runs: by default (--mode resident) it writes launchd/systemd service files and starts the background daemons; with --mode on-demand it registers no service (see Install without a service) |
Step 1 gets the binaries onto your machine. Step 2 wires them into your coding agent’s hook configuration and, in resident mode, your system services. Both are needed for a working install.
Step 1 — Binary acquisition
Option A: Install script (recommended)
curl -fsSL https://raw.githubusercontent.com/yorch/ai-agents-observability/main/scripts/install.sh | bashOr to install a specific version or to a system-wide directory:
curl -fsSL ... | bash -s -- --version v1.0.0curl -fsSL ... | bash -s -- --prefix /usr/local/bin # system-wide (requires sudo)The default prefix is ~/.local/bin — a user-writable directory that avoids sudo entirely. If it’s not on your PATH, the script prints the export PATH= line to add to your shell profile. The subsequent aiot install command never needs sudo: it writes to user-owned directories only (~/Library/LaunchAgents/, ~/.config/systemd/user/, ~/.claude/, etc.).
Upgrading from a previous install? If you previously installed to
/usr/local/bin(the old default), re-running the script will install to~/.local/bininstead. The old binary will remain at/usr/local/binuntil you remove it (sudo rm /usr/local/bin/aiot /usr/local/bin/aiot-runtime). Or pass--prefix /usr/local/binto keep the old location.
What the script does, in order:
- Parses args —
--version <tag>,--prefix <dir>(default~/.local/bin); validates that values are present and don’t start with-. - Detects platform —
uname -s+uname -m→ one ofdarwin-arm64,darwin-x64,linux-x64,linux-arm64. Exits 1 on unsupported platforms. - Resolves version — if no
--version, queries the GitHub API for the latest release tag. - Downloads the binary — prefers
gh release downloadifghis installed and authenticated, otherwise falls back tocurl. Shows a progress bar for the 50–80 MB download. - Fetches checksums — downloads
SHA256SUMS-hookfrom the same release. A 404 (asset not published for older releases) warns and continues; any other HTTP error or network failure aborts — the binary is not installed without verification. - Verifies checksum — uses
sha256sumon Linux orshasum -a 256on macOS (strips CRLF from the checksums file first). Aborts on mismatch or if neither tool is available. - Detects upgrades — if an existing binary is at the install path, runs
--version(with a 5s timeout) and reports the old version. - Installs —
mvinto the prefix, usingsudoonly if the prefix is not writable. - Quarantine notice — on macOS, if the binary has the
com.apple.quarantinexattr, prints thexattr -dcommand to remove it.
Option B: Manual download
- Go to the releases page.
- Download both binaries for your platform:
| Launcher | Runtime | Platform |
|---|---|---|
aiot-darwin-arm64 |
aiot-runtime-darwin-arm64 |
macOS (Apple Silicon) |
aiot-darwin-x64 |
aiot-runtime-darwin-x64 |
macOS (Intel) |
aiot-linux-x64 |
aiot-runtime-linux-x64 |
Linux (x86-64) |
aiot-linux-arm64 |
aiot-runtime-linux-arm64 |
Linux (ARM64) |
- Download
SHA256SUMS-hookfrom the same release.
Option C: Via the GitHub CLI
TAG=v1.0.0 # replace with the tag you wantgh release download "${TAG}" --repo yorch/ai-agents-observability \ --pattern "aiot-darwin-arm64" \ --pattern "aiot-runtime-darwin-arm64" \ --pattern "SHA256SUMS-hook"Verify (manual download)
sha256sum -c SHA256SUMS-hook --ignore-missingBoth binaries should report OK.
Install manually (manual download)
Install both binaries to the same directory — the launcher finds the runtime by looking for aiot-runtime next to itself. The examples below use ~/.local/bin (the default, no sudo needed). Use /usr/local/bin for a system-wide install (requires sudo).
Mac:
mkdir -p ~/.local/binchmod +x aiot-darwin-arm64 aiot-runtime-darwin-arm64mv aiot-darwin-arm64 ~/.local/bin/aiotmv aiot-runtime-darwin-arm64 ~/.local/bin/aiot-runtimeIf the binary is unsigned (no Apple signing secrets were configured at build time), remove the quarantine attribute from both:
xattr -d com.apple.quarantine ~/.local/bin/aiot ~/.local/bin/aiot-runtimeSigned binaries (codesigned + notarized) do not need this step — Gatekeeper will accept them.
Linux:
mkdir -p ~/.local/binchmod +x aiot-linux-x64 aiot-runtime-linux-x64mv aiot-linux-x64 ~/.local/bin/aiotmv aiot-runtime-linux-x64 ~/.local/bin/aiot-runtimeStep 2 — Delivery setup and hook wiring
Once the binary is on your PATH, run:
# Persist these first when the platform is not running on localhost.aiot config set web-url https://observability.example.comaiot config set ingest-url https://ingest.example.com
aiot login # GitHub device-code OAuth flowaiot install # resident (default): launchd/systemd services + auto-wires detected agents # or: aiot install --mode on-demand (no service; see below)aiot status # verify everything is healthyWhat aiot install does, in order:
- Guards against uncompiled use — if
process.execPathis the Bun runtime (not the compiled binary), refuses to run. Service files would point at the wrong executable and agent hooks would be written asbun hook <kind>, which no agent can run.--force --no-autowrites the service files only (resident mode; with--mode on-demandit records the mode, writes nothing and wires no hooks); agent hooks are never wired from the Bun runtime. - Records the mode and stops any running delivery process —
--modeif given, otherwise the mode already recorded (residentwhen none is). A running drainer, resident flusher or shipper, oraiot importholding the delivery lease is stopped first. In on-demand mode it also removes any resident service units left from an earlier install. - Writes service files (resident mode only; on-demand writes none):
- macOS:
~/Library/LaunchAgents/com.brnby.aiot.{flusher,shipper}.plist(launchd) - Linux:
~/.config/systemd/user/aiot-{flusher,shipper}.service(systemd user units)
- macOS:
- Handles upgrades — if service files already exist, unloads/disables them first, then rewrites and reloads. This makes
installidempotent — re-running it after a binary upgrade restarts the daemons cleanly. - Starts the services (resident mode, default
--start): runslaunchctl load/systemctl --user enable --now. If any start step fails, exits 1 with a clear error. Use--no-startto write files without starting (prints the commands instead). - Auto-detects and wires agent harnesses — scans for installed agents (Claude Code, Codex, Gemini CLI, Copilot CLI, Pi, OMP, opencode) and automatically writes hook configuration into each detected agent’s config. In interactive mode, shows a checkbox list of detected agents; use
--yesto wire all without prompting. For shared config files, creates a.aiot-backupbefore first modification, preserves user-defined hooks, and strips only aiot-owned entries on re-install (idempotent). A symlinked config file (a dotfile manager) is updated through the link, which stays in place and whose target keeps its permissions; a target in a read-only location (a Nix store) makes the install fail with a message naming it. A symlinked parent directory that cannot take a new file is named in the message too. Writes are atomic but not locked: if the agent or you rewrite the same file at the same instant, the later write can drop the earlier one’s change, and re-runningaiot installrepairs it. Also not preserved: hard links (the rename breaks them), ownership, ACLs and xattrs; a dangling symlink whose parent directories are missing gets them created. Codex’sconfig.tomlis still edited in place, not atomically. Agents that are not detected get their snippet printed for manual setup.
| Flag | Description |
|---|---|
--no-start |
Write service files but don’t load/enable them (prints the commands instead) |
--force |
With --no-auto, write service files even when running uncompiled (from the Bun runtime, not the binary). Agent hooks are never wired from the Bun runtime, with or without --force |
--yes |
Wire all detected agents without prompting |
--agent <name> |
Wire only this agent (repeatable); skips detection and prompting |
--no-auto |
Skip auto-wiring entirely; print snippets for all agents (legacy behavior) |
--dry-run |
Show what would be wired without modifying any files |
--mode resident|on-demand |
resident (default): launchd/systemd services. on-demand: no service units; a short-lived drainer runs after agent activity |
Install without a service (--mode on-demand)
If you do not want a launchd/systemd service (on macOS it triggers a Login Items prompt and a permanent background process), install in on-demand mode:
aiot install --mode on-demandThis wires the same agent hooks but writes no service units and runs no
launchctl/systemctl. After a Stop / SubagentStop / SessionEnd hook that queued
something (or a SessionStart, as catch-up), the hook starts a short-lived detached
aiot drain, which redacts and ships exactly what the resident services would.
Switching between modes (--mode resident / --mode on-demand) stops and removes
the other mode’s units and any running drainer.
In short: nothing resident runs between agent sessions (a drainer can live up to
120 s after the last hook); data that could not be delivered waits on disk for the next
agent session (kept for 7 days); transcripts ship less often than in resident mode;
late events can miss PR linking; and a hard container teardown can lose the last
batch unless aiot drain --wait runs first. The exact rules, and the macOS note (the
MDM pre-approval idea is untested advice), are in
apps/hook/README.md.
aiot status in this mode shows the age of the oldest queued row, the last clean
drain, the current lease holder, and the queue depth (there is no heartbeat to go
stale).
Containers and devcontainers. Use on-demand mode with AIOT_HOME (and the config
directory) on a volume, a token from the environment, an explicit
aiot config set ingest-url ... (drainers ignore INGEST_BASE_URL from the
environment), and aiot drain --wait as the pre-stop step. The recipe, with the
reason for each line, is in
apps/hook/README.md.
After login, historical sessions can be previewed without uploading:
aiot import --agent codex --dry-run# Also supported: claude-code, opencode, pi, ompSee apps/hook/README.md for the full CLI reference.
Air-gapped distribution
For air-gapped environments, download both binaries and SHA256SUMS-hook on a connected machine, transfer via your approved mechanism, verify checksums on the target, and install manually as described in Step 1 Option B above. Then run Step 2 (aiot install) on the target machine.
Updating
# Option A: re-run the install script (detects the upgrade, replaces both binaries)curl -fsSL https://raw.githubusercontent.com/yorch/ai-agents-observability/main/scripts/install.sh | bash
# Option B: manualgh release download v1.1.0 --repo yorch/ai-agents-observability \ --pattern "aiot-darwin-arm64" \ --pattern "aiot-runtime-darwin-arm64" \ --pattern "SHA256SUMS-hook"sha256sum -c SHA256SUMS-hook --ignore-missingchmod +x aiot-darwin-arm64 aiot-runtime-darwin-arm64mv aiot-darwin-arm64 ~/.local/bin/aiotmv aiot-runtime-darwin-arm64 ~/.local/bin/aiot-runtimeAfter replacing the binaries, re-run aiot install to restart the daemons with the new executable (resident mode). In on-demand mode there are no daemons to restart: the next drainer simply runs the new binary, and install only re-applies the hook wiring.
aiot install # resident: unloads old services, rewrites files, reloads. on-demand: re-wires hooks onlyaiot status # verify: the mode line, and in resident mode that the daemons are runningaiot install without --mode keeps the mode you installed in (resident when none
is recorded). On an --mode on-demand install it therefore writes no service units and
starts no daemon — it only re-wires the hooks (and any newly installed agent) at the new
binary’s path. To change mode, say so: aiot install --mode resident /
--mode on-demand.
The hook binary is stateless across versions — the local SQLite queue, identity, and service files are preserved.