Skip to content

CLI

The HollowHost CLI lets you manage your AI Jobs, trigger runs, view logs, and configure secrets from your terminal.

Installation

curl -L https://download.hollowhost.com/latest/hollowhost_darwin_arm64.tar.gz | tar xz
sudo mv hollowhost /usr/local/bin/
curl -L https://download.hollowhost.com/latest/hollowhost_darwin_amd64.tar.gz | tar xz
sudo mv hollowhost /usr/local/bin/

Linux

Beta

Linux builds are provided as-is and have not been extensively tested. Please report any issues.

curl -L https://download.hollowhost.com/latest/hollowhost_linux_amd64.tar.gz | tar xz
sudo mv hollowhost /usr/local/bin/
curl -L https://download.hollowhost.com/latest/hollowhost_linux_arm64.tar.gz | tar xz
sudo mv hollowhost /usr/local/bin/

Verify the installation:

hollowhost version

Upgrade

The CLI checks for new releases at most once a day and prints a notice on stderr when your binary is outdated. To upgrade, re-run the installation command for your platform — it always fetches the latest release.

Which version you need

hollowhost version prints your version. The swarms commands, swarms flows included, need 1.23.0 or later; an older CLI answers unknown command "swarms". Creating and editing a flow (swarms flows create, export, update, rename, schedule, delete and validate) needs 1.24.0 or later.

Authentication

Log in with your HollowHost account. This opens your browser for secure authentication via OAuth 2.0.

hollowhost login

On headless environments (SSH, CI), use --no-browser to get a URL you can open manually:

hollowhost login --no-browser

To log out and remove stored credentials:

hollowhost logout

Commands

AI Jobs

agents is still accepted

The command group is hollowhost ai-jobs. The legacy hollowhost agents is kept as an alias during the transition, so existing scripts keep working.

List AI Jobs

hollowhost ai-jobs list

All commands support --output json for scripting (see JSON output):

hollowhost ai-jobs list -o json

Get AI Job details

hollowhost ai-jobs get <ai-job-id>

Create an AI Job

hollowhost ai-jobs create \
  --repo owner/repo \
  --lang python \
  --pm uv \
  --token-type NONE
Flag Required Description
--repo Yes GitHub repository (owner/repo)
--lang Yes python or typescript
--pm Yes Package manager: pip, uv (Python) or npm (TypeScript)
--token-type No CUSTOM (default), DEFAULT, or NONE; see Repository access
--github-token If CUSTOM GitHub personal access token
--entry-point No Entry point file (e.g. src/main.py)
--branch No Git branch to deploy (default: repo default branch)
--sub-path No Sub-directory to deploy for mono-repos (default: repo root)
--project No Project ID to assign the AI Job to
--swarm No Swarm ID to add the AI Job to (independent from --project)
--follow No Watch validation progress

When --sub-path is set, the AI Job is built from that sub-directory and --entry-point stays relative to the sub-path (write src/main.py, not apps/agent-meteo/src/main.py).

# Deploy the apps/agent-meteo sub-directory from the dev branch of a mono-repo
hollowhost ai-jobs create \
  --repo owner/mono-repo \
  --lang python \
  --pm uv \
  --branch dev \
  --sub-path apps/agent-meteo \
  --entry-point src/main.py

No name at creation. create takes no --name: a new AI Job shows as its repository's name until you give it one with hollowhost ai-jobs update <ai-job-id> --name "..." (or on the AI Job's Settings tab in the web app).

Set for good at creation: the repository, branch, sub-path, language and package manager. The CLI, the API and the web app can't change them afterwards; to deploy another branch or sub-path, create another AI Job (duplicate copies them as they are).

Repository access

--token-type says how HollowHost reads your repository: to check it when the AI Job is created, and to build it at each deploy.

Value For Token used
NONE A public repository None
DEFAULT Private repositories, with one token for all your AI Jobs The GitHub token saved on your account
CUSTOM (the CLI's default) A private repository with a token of its own --github-token, kept for this AI Job only

The account's token is set in the web app, under Settings → GitHub Token; the CLI can't set it. Use a fine-grained token with Contents: Read-only on your repositories, or a classic token with the repo scope (ghp_… or github_pat_…). It's read at each check and each deploy, so a new token applies to every DEFAULT AI Job at its next deploy. Without one, a DEFAULT AI Job ends in VALIDATION_ERROR: No default GitHub token configured in your profile.

Deploy an AI Job

hollowhost ai-jobs deploy <ai-job-id>

# Watch the deployment in real time
hollowhost ai-jobs deploy <ai-job-id> --follow

Deploy an AI Job once it's VALIDATED. Deploy it again to ship new commits or settings (from DEPLOYED), or to retry after an error. Don't deploy while it's VALIDATING or DEPLOYING: the deploy isn't refused, and it starts a second build.

While it deploys, the AI Job is DEPLOYING and refuses runs, scheduled ones included. A failed deploy leaves DEPLOYMENT_ERROR, which refuses runs until a deploy succeeds.

Right after the first deploy, an AI Job can show DEPLOYED a few seconds before it can run. A run started then waits up to 20 seconds; if the AI Job still isn't ready, the run is refused with Agent Lambda is still initializing (503): start it again a little later.

Run an AI Job

hollowhost ai-jobs run <ai-job-id>

# Watch execution and display logs when done
hollowhost ai-jobs run <ai-job-id> --follow

# Pass an event payload — exposed to your code as the HH_EVENT env var (JSON string)
hollowhost ai-jobs run <ai-job-id> --event '{"mode": "backfill", "day": "2026-07-14"}'
hollowhost ai-jobs run <ai-job-id> --event-file event.json

At runtime your code also receives HH_RUN_ID (the run's identifier), HH_SOURCE (api, scheduler, or flow for a step of a flow), and — for scheduled runs — HH_SCHEDULED_TIME (the ISO timestamp the schedule fired at). A step of a flow gets flow even when a schedule started the flow: code that checks for scheduler should check for flow too. See Event payload format for the full contract (envelope, 128 KB cap, error codes, runtime examples). In the web app, the same payload can be sent with Run with event… on the AI Job page.

Only a DEPLOYED AI Job runs, one run at a time. A start while a run is going is refused: it is never queued, and never runs alongside.

  • ai-jobs run (like Run in the web app, or the API) gets 409 RUN_ALREADY_IN_PROGRESS.
  • A start from the AI Job's schedule is skipped.
  • A step of a flow fails (see Swarm Flows).

Each refused start shows in the AI Job's runs as canceled, with its reason.

Update an AI Job

hollowhost ai-jobs update <ai-job-id> \
  --name "My AI Job" \
  --memory-size 512 \
  --timeout 300
Flag Description
--name Display name (max 50 chars)
--description Description (max 200 chars)
--memory-size Memory: 256, 512, or 1024 MB
--timeout Timeout: 60-900 seconds
--entry-point Entry point file path
--persistent-storage / --no-persistent-storage Enable / disable persistent storage
--token-type CUSTOM, DEFAULT, or NONE
--github-token GitHub token (when --token-type CUSTOM)
--project Project ID (empty string to unassign)
--swarm Swarm ID (empty string to make it standalone); see Swarms
--yes With --swarm, move it from another swarm without asking
--schedule Cron expression, 5 fields (e.g. "0 9 * * MON-FRI"); see Schedule an AI Job
--schedule-timezone Timezone (e.g. Europe/Paris)
--enable-schedule / --disable-schedule Switch the schedule on or off, keeping its cron and time zone
--remove-schedule Remove the schedule entirely

When a change applies

Change Applies
Environment variables, secrets At the next run
Schedule, name, description, project, swarm Right away
Memory, timeout, persistent storage, entry point At the next deploy
--token-type The repository is checked again: see below

update prints ✓ AI Job updated. either way: after a change that needs it, run hollowhost ai-jobs deploy <ai-job-id>.

Changing --token-type, even to the same value, checks the repository again: the AI Job goes back to VALIDATING, then VALIDATED (or VALIDATION_ERROR), and is no longer DEPLOYED. Its runs and its schedule are refused until you deploy it again.

Schedule an AI Job

hollowhost ai-jobs update <ai-job-id> --schedule "0 8 * * MON-FRI" --schedule-timezone Europe/Paris --enable-schedule
hollowhost ai-jobs update <ai-job-id> --disable-schedule   # keeps the cron, to switch it back on later
hollowhost ai-jobs update <ai-job-id> --remove-schedule

The schedule is a 5-field cron, minute hour day-of-month month day-of-week, read in its time zone:

  • Days of the week are 0 to 6 from Sunday (7 is Sunday too), or their names in any case: SUN, MON-FRI, sat,sun.
  • Set the day of the month or the day of the week, not both, and no step (/) in the day of the week. A schedule that can't run is refused with INVALID_SCHEDULE.
  • Each flag changes only what it names. --enable-schedule alone switches the schedule on without touching its cron or time zone; --schedule alone keeps the time zone and on/off state.
  • A first schedule is in UTC and switched off, unless you also pass --schedule-timezone and --enable-schedule.
  • A schedule starts runs only while the AI Job is DEPLOYED.

Before 1.23.0 (October 28, 2026), a first schedule saved switched off was dropped without an error, and --enable-schedule, --disable-schedule or --schedule-timezone used alone removed the schedule. The fix is on HollowHost's side, so it applies whatever your CLI version.

Delete an AI Job

It deletes the AI Job and what goes with it: its builds, logs, secrets and stored files.

hollowhost ai-jobs delete <ai-job-id>

# Skip confirmation
hollowhost ai-jobs delete <ai-job-id> --yes

Duplicate an AI Job

hollowhost ai-jobs duplicate <ai-job-id>

Environment variables

Import variables from a .env file. An interactive picker lets you classify each variable as an environment variable or a secret. Sensitive names (containing KEY, SECRET, TOKEN, PASSWORD, etc.) are pre-classified as secrets.

hollowhost ai-jobs env import <ai-job-id> --file .env

Non-interactive mode for CI/scripts:

# Specify which variables are secrets
hollowhost ai-jobs env import <ai-job-id> --file .env --secrets "API_KEY,DB_PASSWORD"

# All as env vars
hollowhost ai-jobs env import <ai-job-id> --file .env --all-env

# All as secrets
hollowhost ai-jobs env import <ai-job-id> --file .env --all-secrets

Each list an import sends replaces the stored one. The file's variables and its secrets go as two lists:

  • a variable or secret that isn't in the file is removed from its list;
  • a list the import doesn't send is kept: with --all-env, or when you mark nothing as a secret, your secrets stay as they are (and your variables with --all-secrets);
  • so an import can't empty a list.

Empty values are imported empty: FOO= gives FOO an empty value, it isn't skipped.

A name can't be both a variable and a secret: the import is refused with 400, Secret names conflict with environment variable names: …. An import of secrets only is compared with the variables already stored. To turn a variable into a secret, import a file that has all your variables, with that one marked as a secret.

Variables and secrets apply at the next run, with no deploy.

Note

Values are never displayed in the terminal. Only variable names are shown.

Runs

# List runs for an AI Job
hollowhost runs list <ai-job-id>

# View logs for a specific run
hollowhost runs logs <ai-job-id> <run-id>

Projects

hollowhost projects list
hollowhost projects create --name "My Project"
hollowhost projects rename <project-id> --name "New Name"
hollowhost projects delete <project-id> --yes

A project files AI Jobs (ai-jobs create --project, ai-jobs update --project) and swarms (swarms create --project, swarms update --project) to sort them in the web app; ai-jobs list shows each AI Job's project.

  • Names don't have to be unique (1 to 50 characters): two projects can have the same name, so scripts should use the project's id, which projects list shows.
  • Deleting a project is refused while AI Jobs are in it (409): take them out first with hollowhost ai-jobs update <ai-job-id> --project "". Swarms never block it: they are left with no project.

Swarms

A swarm groups AI Jobs that work together (for example a chain of jobs that prepare, write and publish content) so you can follow them as one unit. It never changes the AI Jobs themselves: their project, schedule, secrets and runs stay as they are, and an AI Job belongs to at most one swarm.

hollowhost swarms create --name seo-swarm --description "SEO growth swarm"
hollowhost swarms create --name seo-swarm --project <project-id>   # files the swarm in a project
hollowhost swarms list                      # name, AI Jobs, id, health
hollowhost swarms get <swarm-id>            # members with their schedule and last run
hollowhost swarms add-job <swarm-id> <ai-job-id> [<ai-job-id>...]
hollowhost swarms remove-job <swarm-id> <ai-job-id>
hollowhost swarms runs <swarm-id> --limit 50
hollowhost swarms update <swarm-id> --name new-name
hollowhost swarms update <swarm-id> --project ""   # takes it out of its project
hollowhost swarms delete <swarm-id> --yes   # the AI Jobs are kept and become standalone

A swarm's project files the swarm itself: the web app's swarm list can be filtered by it. It never applies to the swarm's AI Jobs, which keep their own project; swarms get shows both.

Names: a swarm's name is unique among your swarms, ignoring case, so SEO and seo can't both exist (409 SWARM_NAME_TAKEN). A flow's name is unique within its swarm (409 FLOW_NAME_TAKEN). AI Job and project names don't have to be unique: scripts should use ids.

Health is OK when the last run of every member is SUCCESS. A member whose last run failed or was canceled makes the swarm DEGRADED, or DOWN when no member succeeded. Members that never ran are ignored; a swarm with no run yet is PENDING.

Moving an AI Job that already belongs to another swarm asks for confirmation, and nothing is changed if you answer no. Pass --yes in scripts:

hollowhost swarms add-job <swarm-id> <ai-job-id> --yes
hollowhost ai-jobs update <ai-job-id> --swarm <swarm-id> --yes
hollowhost ai-jobs update <ai-job-id> --swarm ""   # make it standalone again

hollowhost ai-jobs list shows a SWARM column and accepts --swarm <id-or-name> to list one swarm's AI Jobs; hollowhost ai-jobs get shows the swarm, and -o json includes swarmId and swarmName.

Flows

A flow runs some of a swarm's AI Jobs together, in the order its links give. Draw it in the web app's flow editor, or write it as JSON and save it with the CLI (see Create and edit a flow).

hollowhost swarms flows list <swarm-id>       # name, steps, schedule, id, last run
hollowhost swarms flows get <flow-id>         # each step in order: when it starts, what follows
hollowhost swarms flows run <flow-id>         # like Run flow; prints the flow run's id
hollowhost swarms flows run <flow-id> --event '{"day": "2026-07-14"}'   # the first steps get it as HH_EVENT
hollowhost swarms flows runs <flow-id>        # newest first, with the steps counted by state
hollowhost swarms flows runs <flow-id> --steps --limit 5   # each step: state, duration, AI Job run

flows run takes --event or --event-file, like ai-jobs run. It is refused while the flow runs (FLOW_ALREADY_RUNNING) or past its start limit (FLOW_RATE_LIMITED).

A step's result, the JSON it wrote to HH_RESULT_PATH for the next step (see Pass a result to the next step of a flow), shows in the web app, in the step's panel on the flow page; the CLI doesn't show it. flows runs --steps gives each step's AI Job run id, and hollowhost runs logs <ai-job-id> <run-id> shows its log.

More runs: swarms runs and swarms flows runs show one page, newest first (50 and 20 runs by default; --limit up to 200 and 50). When there are more, the last line prints the command for the next page, with --next-token.

Deleting an AI Job, or taking it out of its swarm, is refused while it is a step of a flow: the error lists those flows, each with its flows get command. Take it out of them in the web app's flow editor, or with flows export and flows update.

Create and edit a flow

hollowhost swarms flows create <swarm-id> --file flow.json   # prints the new flow's id
hollowhost swarms flows export <flow-id> > flow.json         # the flow as JSON, with its version
hollowhost swarms flows update <flow-id> --file flow.json    # saves the whole flow from the file
hollowhost swarms flows rename <flow-id> --name "Evening briefing"
hollowhost swarms flows schedule <flow-id> --cron "0 7 * * 1-5" --timezone Europe/Paris --enable
hollowhost swarms flows schedule <flow-id> --disable         # the flow starts only with Run flow
hollowhost swarms flows delete <flow-id>                     # its AI Jobs and their runs are kept
hollowhost swarms flows validate --file flow.json            # checks the file offline, saves nothing
  • The file is the API's flow JSON: name, nodes (the steps), edges (the links) and schedule. The CLI sends it as is and the API checks it: a refusal prints the API's message and code, like [FLOW_HAS_CYCLE] (see Errors). --file - reads it from stdin.
  • To edit a flow, export it, change the file, then update it. version tells the API which save you changed: if the flow was saved since, update is refused (409 FLOW_VERSION_CONFLICT), and you export it again and redo your change. flowId, swarmId, createdAt and updatedAt are read only; a file without schedule keeps the flow's schedule.
  • To copy a flow, export it, change its name, then create it.
  • rename and schedule need no export: they don't take the version, like the web app's Schedule switch, so a save made since doesn't refuse them. schedule changes only what you pass (--enable keeps the cron, --cron keeps the time zone), and --remove removes the schedule.
  • A new schedule is off unless you pass --enable, or the file says "enabled": true. While the flow's schedule is on, create, update and schedule name the AI Jobs that no longer run on their own schedule (the steps with turnOffOwnSchedule).
  • delete asks first: pass --yes in scripts.
  • validate checks a file with the API's rules without calling it: its fields, the shape of its schedule and its graph (1 to 20 steps, each AI Job once, no link twice, no loop). It lists every problem, in the API's words, and exits with code 1 if there is one. It can't check what only the API knows (that the AI Jobs are in the swarm, that the name is free, your plan, the cron and the time zone), and create and update don't run it: the API checks the file they send.
# Change the second step's wait in one line
hollowhost swarms flows export <flow-id> | jq '.nodes[1].waitMinutes = 10' | hollowhost swarms flows update <flow-id> --file -

Storage

Persistent storage keeps files from one run of an AI Job to the next. Turn it on with hollowhost ai-jobs update <ai-job-id> --persistent-storage, then deploy the AI Job.

  • Where: your code reads and writes the folder named in the PERSISTENT_STORAGE_PATH environment variable (/tmp/data).
  • When it's saved: before each run, the folder is filled with the saved files. After a run that succeeds, the folder is saved as it is, so the files your code deleted are deleted too. A run that fails saves nothing.
  • Size: up to 1 GiB per AI Job. A run that leaves more fails with StorageQuotaExceeded and saves nothing. Today the folder sits on the AI Job's 512 MB /tmp disk, so keep it under that.
  • Turning it off (--no-persistent-storage, then deploy) keeps the files: the AI Job stops loading and saving them, and storage files lists nothing. Turn it back on and deploy, and they are back. storage clear erases them.
  • Deleting the AI Job erases its files.
  • Moving to a plan without persistent storage, or cancelling your subscription, turns it off and erases the files of every AI Job whose storage is on at that moment. An AI Job whose storage was already off keeps its files.
# View storage info
hollowhost storage info <ai-job-id>

# List files
hollowhost storage files <ai-job-id>

# Delete a file
hollowhost storage delete-file <ai-job-id> path/to/file.json

# Clear all files
hollowhost storage clear <ai-job-id> --yes

Shell completion

Enable autocompletion for your shell:

Add to your ~/.zshrc:

source <(hollowhost completion zsh)

Add to your ~/.bashrc:

source <(hollowhost completion bash)
hollowhost completion fish | source

Restart your shell or run source ~/.zshrc (or equivalent) for changes to take effect.

Global flags

Flag Description
-o, --output Output format: text (default) or json; see JSON output
-h, --help Show help for any command

JSON output

With -o json, a command prints the API's answer as is, on stdout. Its fields are the API's: an AI Job's id is id in ai-jobs get and ai-jobs list, and agentId elsewhere.

Commands Print
ai-jobs list / get, runs list / logs, projects list / create / rename, swarms list / get / create / update / runs, swarms flows list / get / run / runs, storage … {"data": {…}}: one object (agent, project, swarm, flow, flowRun…) or a list (agents, runs, projects, swarms, flows, flowRuns, files), with nextToken when a list has more pages
ai-jobs create, ai-jobs duplicate {"message": "Success!", "agentId": "…"}
ai-jobs run {"data": {"runId": "…", "agent": {…}}}
ai-jobs deploy, ai-jobs update, ai-jobs delete {"message": "…"}; update adds previousSwarmId when you pass --swarm
ai-jobs env import {"envVarsCount", "secretsCount", "envVarNames", "secretNames"}: names, never values
swarms add-job {"results": […]}: the API's answer for each AI Job, {"data": {"agentId", "swarmId", "previousSwarmId"}}
swarms flows create / update / rename / schedule {"data": {"flow": {…}, "ownSchedulesTurnedOff": […], "ownSchedulesNotTurnedOff": […]}}: the flow as saved, then the AI Jobs' ids
swarms flows export The flow alone, {"flowId", "name", "nodes", …}, with or without -o json
swarms flows validate Text, with or without -o json: the problems go to stderr, and the exit code says if there are any
swarms delete, swarms remove-job, swarms flows delete, projects delete Text: the API sends nothing back
  • Errors print on stderr, never on stdout, like Error: API error (409): … [RUN_ALREADY_IN_PROGRESS], and the command exits with code 1. When there is a code in brackets, it's the one to test for.
  • --follow: ai-jobs deploy --follow and ai-jobs run --follow print text, even with -o json; ai-jobs create -o json prints its answer and doesn't follow.
  • Confirmations (ai-jobs delete, projects delete, swarms delete, swarms flows delete, storage clear, moving an AI Job between swarms) ask on stdout: pass --yes in scripts.