Capture agent sessions on a machine with no browser and no interactive user: a CI runner, a container, or a long-lived server. Enrollment happens with a token minted ahead of time on your own machine, so the headless machine never signs in.
Two things make headless different from a laptop install:
- No browser sign-in. Enrollment uses a pre-minted token with
hyp joininstead ofhyp remote login. - No service manager. Container runners usually lack launchd and
systemd, so the daemon runs as a foreground process that your CI shell
or supervisor backgrounds, via
hyp join --no-daemonplushyp daemon run --foreground.
Every run that joins with one token lands under one shared gateway on the server, so a pipeline's runs stay grouped together. A token-based join forwards immediately: there is no first-sync review hold, because whoever minted the token chose enrollment deliberately. See what HypAware records and how to control it.
On your own machine, where you are signed in (hyp remote login):
hyp remote mintThis prints the token once; store it in your CI secret store immediately
(the examples below call it HYP_CI_TOKEN). Only the token goes to standard
output, so hyp remote mint > ci.token captures exactly the secret. Options:
--label <label>names the gateway the token is bound to, for example the pipeline name.--expires-days <n>overrides the 365-day default expiry.
The token never rotates. When it nears expiry, mint a new one and swap the CI secret. Minting binds a new gateway row at mint time (the id is printed on standard error next to the token), so runs before and after the swap group under different gateways, and the old row stays in place server-side.
Three steps, all in the run's shell. The join URL is the server base (not a
/v1/mcp query URL), and the token goes in on standard input so it never
appears in ps output or set -x traces:
# setup
printf '%s' "$HYP_CI_TOKEN" | hyp join https://hyp.example.com --no-daemon
hyp daemon run --foreground &
# ... the job's agent steps run unchanged ...
# teardown: flush what the schedule has not exported yet
hyp sync --yesThe teardown step matters. Sinks export on a schedule, and the most valuable
rows land when the agent session ends, seconds before the runner dies, so no
schedule can be trusted to drain the tail. Always run hyp sync --yes as the
final step, including on failed jobs.
jobs:
agent:
runs-on: ubuntu-latest
env:
HYP_CI_TOKEN: ${{ secrets.HYP_CI_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Start HypAware capture
run: |
npm install -g hypaware
printf '%s' "$HYP_CI_TOKEN" | hyp join https://hyp.example.com --no-daemon
hyp daemon run --foreground &
- name: Run the agent
run: |
# the job's agent steps, e.g.
# claude -p "review the diff"
- name: Flush captured rows
if: always()
run: hyp sync --yesif: always() runs the flush even when the agent step fails, which is often
the run you most want recorded. HypAware requires Node 22.12 or newer.
A server or VM that outlives one job uses the same join, but lets the daemon install as a real service where one is available:
printf '%s' "$HYP_CI_TOKEN" | hyp join https://hyp.example.comWithout --no-daemon, join installs and starts the daemon under launchd or
systemd. Both are per-user services, not system ones: on Linux it is a systemd
user unit, so a headless host needs loginctl enable-linger <user> for it
to start at boot and to survive the last session logging out, and on macOS it
is a LaunchAgent, which needs a logged-in user session. In a container image or a host without a
service manager, keep --no-daemon and run hyp daemon run --foreground as
the entrypoint or under your own supervisor. No teardown flush is needed on a
machine that keeps running; the scheduled exports drain it. Flush with
hyp sync --yes before deliberately retiring the machine.
hyp statuson the runner reports whether recording is active, and what is shared with your team versus kept on the machine.hyp remote mintfailing with HTTP 404 means the server predates the mint endpoint; upgrade the server.- A join that hangs is waiting on standard input: no token was piped in, and
no positional token or
--token-filewas given. - Join never contacts the server, so a query-target URL (
.../v1/mcp) instead of the server base, or an expired token, still joins cleanly and surfaces later as a daemon bootstrap failure.
Command details live in the CLI reference, and the enrollment model for interactive machines in the team setup guide.