Skip to main content

harnesscli Reference

harnesscli is the terminal client for the go-code harness. It speaks to a running harnessd server over HTTP and lets you do everything from sending a one-shot prompt and watching events stream in real time, to listing past runs, continuing multi-turn conversations, replaying recorded rollouts, and running the autoresearch improvement loop — all without leaving your shell.

The key design split: harnessd owns the model, the tools, and all run state; harnesscli is a thin client that formats output for humans and scripts.


Installation

harnesscli is built alongside harnessd by the standard install script:

bash scripts/install.sh # installs to ~/.local/bin by default
bash scripts/install.sh --system # installs to /usr/local/bin

Or build directly:

go build -o harnesscli ./cmd/harnesscli

For isolated development worktrees, scripts/init.sh builds harnesscli into .tmp/bootstrap/bin/harnesscli and exports HARNESS_CLI_BINARY in the generated dev.env file.


Default streaming run mode

When you invoke harnesscli with no recognized subcommand — typically just --prompt and other flags — it runs in streaming run mode:

  1. POST /v1/runs to create the run.
  2. Print run_id=<id> to stdout.
  3. Stream SSE events from GET /v1/runs/{id}/events until a terminal event arrives.
  4. Print terminal_event=<event_type> and exit.

Terminal events are run.completed, run.failed, and run.cancelled. Every non-terminal event is printed as a single line: <event_type> <full_event_json>.

Exit codes

The process exit code reports the run's outcome, so scripts and CI can branch on $?:

CodeMeaning
0run.completed — the run finished successfully
1Client-side error: bad flags, missing prompt, connection/HTTP/stream failure
2run.failed — a turn failed server-side
3Blocked — the run needs input it will never get headlessly (run.waiting_for_user, tool.approval_required, or plan.approval_required observed while stdin is non-interactive)
6run.cancelled — interrupted but resumable via harnesscli continue <run-id> <prompt>
130SIGINT/SIGTERM while streaming

The run_id= / terminal_event= stdout lines are unchanged by this mapping. See Exit Codes for the full contract — blocked-signal details, goal-status reservations, and per-command coverage.

Flags

FlagDefaultDescription
-base-urlhttp://localhost:8080Harness API base URL
-prompt(required)Prompt to send
-model""Model override for this run
-system-prompt""System prompt override
-agent-intent""Startup intent for prompt routing (e.g. code_review)
-task-context""Task context injected into the startup prompt
-prompt-profile""Prompt profile override for model routing
-prompt-custom""Custom prompt extension text
-workspacecwdWorkspace directory for this run
-tuifalseLaunch the interactive BubbleTea TUI (requires a real terminal)
-list-profilesfalseList available profiles and exit
-prompt-behavior(empty)Behavior extension IDs — repeatable or comma-separated
-prompt-talent(empty)Talent extension IDs — repeatable or comma-separated

Go's flag package accepts both -flag and --flag forms; examples in this page use a single dash to match the source.

Example: run and watch events

# Start harnessd in fake (key-free) mode first:
HARNESS_PROVIDER=fake \
HARNESS_FAKE_TURNS=turns.json \
HARNESS_AUTH_DISABLED=true \
go run ./cmd/harnessd &

# Stream a run:
harnesscli -prompt "What is 2+2?"

Output:

run_id=<uuid>
run.started {"id":"evt_...","run_id":"...","type":"run.started","timestamp":"2026-...Z","payload":{"prompt":"What is 2+2?"}}
run.step.started {"id":"evt_...","run_id":"...","type":"run.step.started","timestamp":"2026-...Z","payload":{"step":1}}
run.step.completed {"id":"evt_...","run_id":"...","type":"run.step.completed","timestamp":"2026-...Z","payload":{"step":1}}
run.completed {"id":"evt_...","run_id":"...","type":"run.completed","timestamp":"2026-...Z","payload":{"output":"..."}}
terminal_event=run.completed

-workspace defaults to the current working directory via os.Getwd(). The value is serialized as workspace_path in the run creation request, but the server's POST /v1/runs handler decodes into harness.RunRequest, which has no workspace_path field — the value is currently silently ignored server-side. Workspace selection is controlled by workspace_type and profile-level runner configuration, not by this flag.

-tui requires a real terminal. If stdout is a pipe, harnesscli exits with: --tui requires a terminal; pipe output or use without --tui for streaming mode.

Two HTTP clients

The CLI maintains two separate http.Client instances:

  • requestHTTPClient — 60-second timeout, used for all non-streaming HTTP calls (creating runs, fetching status, etc.).
  • streamHTTPClient — no timeout, idle-connection reaping disabled, keep-alives enabled. Used exclusively for the SSE event stream so that long tool-call pauses do not cause the connection to be dropped mid-run.

Run-management subcommands

The non-streaming subcommands below (list, cancel, status, replay, search) exit 0 on success and 1 on error — the terminal-event exit-code mapping does not apply to them since they never observe an event stream. Streaming continue follows the same exit-code contract as the one-shot mode.

list / runs

List recent runs. Both list and runs invoke the same handler.

harnesscli list
harnesscli list -status running
harnesscli list -conversation-id <cid>

API: GET /v1/runs

FlagDefaultDescription
-base-urlhttp://localhost:8080Harness API base URL
-status""Filter: queued, running, completed, or failed
-conversation-id""Filter by conversation ID

Output is a 4-column table (ID, STATUS, MODEL, PROMPT). The prompt column is truncated at 40 characters. When no runs match, the command prints No runs found.


cancel

Cancel a running or queued run.

harnesscli cancel <run-id>

API: POST /v1/runs/{id}/cancel

On success prints: Run <id> cancelling.


status / show

Show the full details of a single run. Both status and show invoke the same handler.

harnesscli status <run-id>
harnesscli show <run-id>

API: GET /v1/runs/{id}

Output includes: ID, Status, Model, Created, Updated, Prompt (truncated at 80 chars), Error, Output, and workflow recap fields when present.


continue

Send a follow-up prompt to an existing run, creating a new run in the same conversation.

# Stream the continuation (default):
harnesscli continue <run-id> Now explain it to a 5-year-old

# Create without streaming:
harnesscli continue -no-stream <run-id> Now explain it to a 5-year-old

API: POST /v1/runs/{id}/continue with body {"prompt": "..."}

The continuation prompt is everything after the run ID, joined with spaces.

FlagDefaultDescription
-base-urlhttp://localhost:8080Harness API base URL
-no-streamfalsePrint only run_id=<id> and exit without streaming events

When -no-stream is false (the default), the new run's events are streamed and terminal_event=<type> is printed on completion; the same exit-code contract as the one-shot mode applies. When -no-stream is true, only run_id=<id> is printed and the exit code stays 0/1 (no terminal event is observed).


replay

Replay a recorded rollout. The rollout can be provided as a run ID (the server locates the JSONL file) or as a direct rollout file path.

# Simulate replay (default):
harnesscli replay <run-id>

# Fork at step 5 and hand off to a live runner:
harnesscli replay -mode fork -fork-step 5 <run-id>

# Detect drift between a recording and a fresh run:
harnesscli replay -detect-drift <run-id>

API: POST /v1/runs/replay

FlagDefaultDescription
-base-urlhttp://localhost:8080Harness API base URL
-modesimulateReplay mode: simulate or fork
-fork-step0Step to fork from (only used when -mode=fork)
-detect-driftfalseRun drift detection during simulate replay

The fork_step field is included in the request payload only when -mode=fork. Output is pretty-printed JSON to stdout.

See the Rollout & Replay reference for details on the underlying replay semantics.


Search runs by substring. This is a client-side operation.

harnesscli search "fix the login bug"
harnesscli search -status completed authentication

search fetches all runs from GET /v1/runs and filters them in-process. There is no dedicated server-side search endpoint. On large run histories this can be slow.

FlagDefaultDescription
-base-urlhttp://localhost:8080Harness API base URL
-status""Pre-filter by status before searching

The query is all positional args joined with spaces. Matching is case-insensitive substring across: ID, conversation_id, tenant_id, model, prompt, output, status, error, and workflow recap fields.


auth login and config files

auth login

Generate a local API key and save it for use with the harness server.

harnesscli auth login
harnesscli auth login -server http://my-harness:9090 -tenant myteam -name laptop
FlagDefaultDescription
-serverhttp://localhost:8080Harness server URL (stored in config, not contacted)
-tenantdefaultTenant ID for the generated key
-namecliHuman-readable label for the key

auth login does not contact the server. The API key is generated locally using store.GenerateAPIKey. The -server flag only controls what URL is written into the saved config file so subsequent requests know where to connect.

On success, auth login:

  1. Writes ~/.harness/config.json (directory mode 0700, file mode 0600).
  2. Prints the file path, the raw key, and a ready-to-use Authorization: Bearer <key> example.

The generated key carries three scopes: store.ScopeRunsRead, store.ScopeRunsWrite, and store.ScopeAdmin.

Config file locations

harnesscli uses two separate config files for different purposes:

~/.harness/config.json — Written by auth login.

{
"server": "http://localhost:8080",
"api_key": "<raw-token>"
}

improve (autoresearch loop)

harnesscli improve wraps the scripts/autoresearch-loop.sh shell script, exposing the autoresearch self-improvement loop as a first-class CLI command.

improve must be run from the repository root. It looks for scripts/autoresearch-loop.sh relative to the current working directory and exits with an error if the file is not found: scripts/autoresearch-loop.sh not found; run from the go-code repository.

Common usage

# Run one autoresearch iteration on a specific code seam:
harnesscli improve -target "internal/harness.Runner.SubmitInput"

# Run three iterations with a 30-second pause between each:
harnesscli improve -iterations 3 -pause 30 \
-target "internal/harness.Runner.SubmitInput"

# Preview the plan without executing:
harnesscli improve -dry-run -target "internal/workflow"

# Run the score suite (tests + race + regression) and exit:
harnesscli improve -score-only

Flags

FlagDefaultDescription
-target(repeatable)Target seam to inspect; may be repeated
-dry-runfalsePrint the planned command without running it
-score-onlyfalseRun the score suite and exit
-iterations"1"Number of autoresearch loop iterations
-pause"0"Seconds to pause between iterations
-report-dir.tmp/autoresearchDirectory for autoresearch reports
-base-urlhttp://localhost:8080Harness API base URL
-profile"full"Run profile sent to harnessd
-prompt-profile"autoresearch"Prompt routing profile
-model""Optional model override
-max-steps"50"Step budget passed to autoresearch runs
-test-cmd./scripts/test-regression.shDefault validation command for unknown targets

The -test-cmd value is passed to the loop script via the HARNESS_AUTORESEARCH_DEFAULT_TEST_CMD environment variable.

-score-only in detail

-score-only runs these three commands in order and exits on the first failure:

go test ./...
go test ./... -race
./scripts/test-regression.sh

This is a fast sanity check that the tree is green before kicking off a longer autoresearch loop.


HTTP endpoints used by the CLI

For reference, here are all the server routes that harnesscli calls:

MethodPathUsed by
POST/v1/runsdefault run mode
GET/v1/runs/{id}/eventsstreaming (run + continue)
GET/v1/runslist, search
POST/v1/runs/{id}/cancelcancel
GET/v1/runs/{id}status / show
POST/v1/runs/{id}/continuecontinue
POST/v1/runs/replayreplay
GET/v1/profiles-list-profiles
GET/v1/runs/{id}/inputask-user (non-TUI, see note)
POST/v1/runs/{id}/inputask-user (non-TUI, see note)

handleAskUserQuestion — the function that calls /v1/runs/{id}/input to handle interactive run.waiting_for_user events — is defined in cmd/harnesscli/askuser.go and tested independently, but is not wired into the non-TUI streaming loop in main.go. Interactive question-answering in streaming mode is not yet available outside the TUI.


Quick reference

# Key-free smoke run (fake provider):
HARNESS_PROVIDER=fake \
HARNESS_FAKE_TURNS=turns.json \
HARNESS_AUTH_DISABLED=true \
go run ./cmd/harnessd &

harnesscli -prompt "Summarize the diff"

Next steps

  • Server setup — See harnessd Reference to learn how to configure and start the server harnesscli connects to.
  • Exit codes — See Exit Codes for the headless exit-code contract used when scripting runs from shell or CI.
  • Event reference — See Events for the full list of SSE event types and their payloads.
  • Rollout & replay — See Rollout & Replay for the underlying replay, fork, and drift-detection semantics.
  • Configuration — See Environment Variables for HARNESS_PROVIDER, HARNESS_FAKE_TURNS, and the full HARNESS_* env var reference.