A spec-driven Go CLI for Harness. Commands are declared in YAML spec files; the framework wires them into Cobra subcommands at startup. Agents rarely need to touch Go code — most work is adding or editing spec files.
Grammar: harness <verb> <noun> [id] [flags] — verb always comes first.
harness create pr
harness list pipeline
harness get pr <repo_id>/<pr_number>
harness execute pr:merge <repo_id>/<pr_number>harness get module <name> # domain model and noun list for a module (e.g. "code", "pipeline")
harness get noun <noun> # fields and available verbs for a specific noun
harness list noun --matrix # all nouns × verbs at a glance
harness <verb> <noun> --help # flags for a specific command| Verb | Purpose |
|---|---|
list |
List resources (paginated) |
get |
Get a single resource by id |
create |
Create a resource |
update |
Update a resource |
delete |
Delete a resource |
execute |
Run/trigger a resource (pipelines, merges, etc.) |
Some commands use a variant suffix to distinguish sub-operations on the same noun:
harness list pr:mine # PRs authored by you
harness execute pr:merge <repo_id>/<pr_number> # merge a PR
harness execute pr:close <repo_id>/<pr_number> # close a PR
harness get pipeline:summary <pipeline_id>Most commands accept --org and --project to override the profile defaults:
harness list pipeline --org my-org --project my-project# Build (requires Task: brew install go-task)
task build # outputs bin/harness and bin/harness-har
# Faster build when only touching core (not HAR) — HAR pulls in large libraries and is slow
task build:main # builds bin/harness only, skips HAR
# Install to GOPATH
go install ./cmd/harness/...
# IMPORTANT: active binary lives at ~/.local/bin/harness, not ~/go/bin/harness
cp $(go env GOPATH)/bin/harness ~/.local/bin/harnesscmd/harness/ # main entry point
pkg/
spec/ # ← primary work area: *.spec.yaml + Go struct types
registry/ # spec → Cobra command wiring (endpoint.go, verb.go, etc.)
endpoint/ # HTTP execution: BuildRequest, HTTPFetchFn, paging strategies
exprenv/ # expr-lang evaluation (flags.*, ctx.*, auth.*, it.*)
client/ # HTTP client (DoRequest, auth injection)
cmdctx/ # Ctx struct threaded through all handlers
format/ # Table/field rendering
tui/ # Interactive UI picker
modules/har/ # External HAR module (separate go.mod)
Each *.spec.yaml declares:
nouns— entity types with theirfields(id, expr, label, field_type, mutable_path, width_max, align).commands—verb noun[:variant]pairs wired tohandler_type: endpoint.
noun: kg
noun_variant: type # → cobra command "kg:type"FullNoun() returns "noun:variant" when variant is set, "noun" otherwise.
| Field | When it applies |
|---|---|
columns: [...] |
Which fields show in list output |
fields_subset: [...] |
Which fields show in get output (on endpoint, not noun) |
| All noun fields | Available to get unless fields_subset limits them |
Fields with mutable_path are writable via --set/--del on update commands. mutable_path is a dot-path relative to the update_body_pick subtree — never starts with it.:
# noun field
- id: name
expr: it.project.name # display expression
mutable_path: name # relative path within the picked subtree
# update command endpoint
update_strategy: get-then-put
update_body_pick: it.data.project # evaluated against root GET response
update_body_wrap: project # re-wraps the mutated object in PUT bodyRules:
update_body_pickshould matchyaml_pick_expron the correspondinggetcommand — they describe the same subtree.- Fields without
mutable_pathare read-only and do not appear in--list-fields. mutable_pathmust not start withit.— the spec validator will reject it.
Paths use Go template syntax evaluated by exprenv:
path: /code/api/v1/repos/{{ctx.parentId}}/branchesAvailable variables: ctx.id, ctx.idParts[N], ctx.parentId, auth.account, auth.org, auth.project, flags.*.
id_parts: 2→ user passes<a>/<b>; available asctx.idParts[0]andctx.idParts[1]. Works forget/execute/delete.requires_parentid: true→ user passes the parent as a positional arg; available asctx.parentId. Used forlist/createwhere the sub-resource doesn't have its own id yet.id_partsis NOT supported bylist.
nilfrom an expression causesbuildBodyto skip the key entirely (use for optional params).??is null-coalescing:it.a ?? it.b ?? {}resolves the first non-null.- Array concat:
concat(it.entity_types ?? [], it.event_types ?? []). - Epoch formatting:
epochMs(it.created)→ human-readable timestamp.
body_params:
options.timeout_ms: 'flags.timeout != "" ? flags.timeout : nil'Dots are expanded into nested objects by setDotPath.
| Strategy | Use case |
|---|---|
page_header |
Harness v1 REST APIs (total in X-Total header) |
flat_list |
gRPC-gateway endpoints that return everything at once |
page_index |
Older Harness APIs with pageIndex/pageSize |
POST to /schema-service/grpc/... or /query-service/grpc/.... Always need:
method: POST
request_headers:
x-tenant-id: auth.accountAn empty body_params still sends {} (required by gRPC-gateway for POST/PATCH/PUT).
- Create
pkg/spec/<module>.spec.yaml. - It is automatically embedded via
//go:embed *.spec.yamlinspec.go. - Set
module_type: builtinandmodule_desc: .... - Add
noun_aliases(at minimum the plural form) to every noun in the spec. - Create
modules/<module>/<module>.help.txt— seemodules/platform/platform.help.txtfor the format. Include a domain-model section and a{{nouns}}placeholder. - Create
modules/<module>/<module>.go— embed the help.txt and callreg.SetHelpText(helpText)inModuleInit. Seemodules/kg/kg.gofor the minimal pattern. - Wire it into
cmd/harness/main-harness.go: add the import and call<module>.ModuleInit(reg.Module("<module>")). - Run
task build && cp $(go env GOPATH)/bin/harness ~/.local/bin/harnessto test.
# Always copy after build
task build && cp $(go env GOPATH)/bin/harness ~/.local/bin/harness
# Faster: use task build:main when only core (non-HAR) code changed
task build:main && cp $(go env GOPATH)/bin/harness ~/.local/bin/harness
# Verify a command
harness list kg:type
harness get kg:type <id>
harness list branch <repo_id>
harness list pr_activity <repo_id>/<pr_number>The CLI reads auth from the active profile (typically ~/.harness/profiles.yaml).
| File | Commands |
|---|---|
aievals.spec.yaml |
list/get/create/delete for eval_dataset, evaluation, eval_run, eval_metric, eval_metric_set, eval_target, eval_model, eval_suite; execute evaluation:run, execute eval_suite:run |
code.spec.yaml |
list/get/create/update/delete repository; list/get/create/update/execute pr (pr:merge, pr:close); list/get/create/delete branch; list/get commit; list/create/delete tag; list pr_activity |
kg.spec.yaml |
list/get kg:type; list kg:queryable_type, kg:related_type, kg:connection; execute hql:grammar, hql:validate, hql:run, hql:explain |
platform.spec.yaml |
Platform resources (projects, orgs, etc.) |
pipeline.spec.yaml |
CI/CD pipelines |
core.spec.yaml |
Core resources |
Do not hardcode into source files, comments, or documentation:
- Account IDs, org IDs, project IDs
- API tokens, OAuth tokens, client secrets
- User emails, UUIDs, or any other PII
- Real hostnames or URLs from live environments (unless they are published public endpoints like
id.harness.io)
Use placeholder text like <accountId>, <token>, <email> in examples.
- Binary not updated:
task buildalone isn't enough — mustcpto~/.local/bin/harness. listwithid_parts: Not supported. Userequires_parentid: trueinstead.- Code API paths: Use bare repo identifier in path (e.g.
/code/api/v1/repos/{{ctx.parentId}}/branches). org/project go as query params automatically — do NOT prefix paths with{{auth.account}}/{{auth.org}}/{{auth.project}}. columnsonget: Ignored. Usefields_subseton the endpoint to filtergetoutput.- gRPC oneof fields: Include all variants in
??chain (entity_type, event_type, metric_type, view_type, relationship_type, config_type).