| name | tfctl |
| description | Interact with HCP Terraform / Terraform Cloud / Terraform Enterprise using the tfctl CLI. Full API coverage.
Use for ANY HCP Terraform or Terraform Cloud or Terraform Enterprise question or action — listing workspaces,
starting/diagnosing runs, reading vars, modifying resources, calling API operations.
|
| license | MPL-2.0 |
tfctl — HCP Terraform CLI
Single binary, full v2 API coverage. Already authenticated.
Hard rules
-
Never pipe tfctl JSON to an external jq. Use the built-in --jq '<expr>' flag — it implies --json and runs gojq on the response envelope.
-
Resolve names with -p, not separate lookup calls. Paths with {workspace}/{team}/{project}/{varset} accept -p workspace=NAME etc. — tfctl resolves name→ID for you. Don't fetch the ID first.
-
Trust the first answer. data: [], data: null, relationships.X.data: null, or stderr "no current run"/"not found" ARE the answer. Don't re-query in another format. Don't walk relationships "to verify".
-
When a named resource is not found, stop completely. Exit code 2 or absence from a listing IS the full answer. Never:
- Try a different resource ID "to verify the endpoint works"
- Pivot to another org/workspace that appeared in the available list
- Explore related resources to find "similar" information
- Use Rule 3 to justify switching to a different resource: if you listed orgs and 'platform' isn't there, the first answer is "platform doesn't exist" — stop, don't use whatever org IS listed instead.
Examples: run-POLICY returns exit 2 → stop, don't query other run IDs. Listing orgs shows no 'platform' → stop, don't use the org that IS listed.
-
Never run tfctl harness exec yourself to self-authorize. The --allow-delete grant is a human's opt-in for your session. If a delete is refused, relay the printed harness exec --allow-delete=<class> command back to the human — do not run it (or set TFCTL_EXEC_SESSION) to grant yourself permission. See Deleting resources.
Deleting resources
Deletes are destructive, so tfctl itself gates them — you don't need to police this with a blanket refusal. A noninteractive tfctl ... -X DELETE only goes through when a human has opted in for this session by launching you via tfctl harness exec --allow-delete=<class> -- <command> (which sets TFCTL_EXEC_SESSION). Otherwise tfctl refuses on its own and tells you what to do.
So when you're asked to delete something, just run the normal command and let tfctl be the gate:
tfctl api PATH -X DELETE
- If the session authorizes that resource's class, it succeeds — that's the human's intent, not a violation; proceed and report the result.
- If it doesn't,
tfctl refuses with a self-documenting message and prints the exact command to hand back (including the harness exec --allow-delete=<class> a human can use to authorize you). Relay that rather than forcing it; don't run harness exec yourself to self-authorize.
- You can't tell in advance whether the session grants a class — so don't guess. Attempt the delete and let the refusal tell you. The refusal is a plain exit 1 with a message naming the class and the
--allow-delete=<class> command; that is your cue to relay. This is a grant gap, NOT an auth failure: never report it as an expired token, an exit code 3, or tell the human to re-login unless tfctl actually says the token is expired/invalid.
- Apply ordinary caution to irreversible deletes (
organizations, projects). For a high-stakes resource, confirm intent with the human first even when the session would allow it.
URL shape: per-workspace subpaths live at /workspaces/{workspace}/...
Most "things attached to a workspace" (vars, runs, varsets, remote-state-consumers, configuration-versions, notification-configurations, state-versions) are under /workspaces/{workspace}/..., NOT /organizations/{org}/workspaces/{name}/.... The org-nested form only exists for the workspace resource itself (/organizations/{org}/workspaces/{name}). For everything else, use /workspaces/{workspace}/X -p workspace=NAME.
Exception: Policy checks and run-specific data live under /runs/{run-id}/..., not under workspace paths. For example: /runs/{run-id}/policy-checks.
Anti-patterns to avoid
These paths do not exist; don't try them:
- ❌
/organizations/{org}/workspaces/{name}/vars — use /workspaces/{workspace}/vars -p workspace=NAME instead
- ❌
/organizations/{org}/workspaces/{name}/state-versions — use /workspaces/{workspace}/state-versions -p workspace=NAME
- ❌
/organizations/{org}/workspaces/{name}/configuration-versions — use /workspaces/{workspace}/configuration-versions -p workspace=NAME
- ❌
/workspaces/{workspace}/policy-checks — use /runs/{run-id}/policy-checks instead
- ❌
/organizations/{org}/varsets (partially wrong path) — use /organizations/{organization}/varsets with correct org placeholder
- ❌ External
jq pipes like tfctl ... | jq '...' — always use tfctl api ... --jq '...' with built-in flag
Cookbook — one-line answers for common tasks
tfctl api /organizations/{organization}/workspaces --page-size 1 --jq '.meta.pagination.["total-count"]'
tfctl api /organizations/{organization}/workspaces -f 'search[name]=TERM' --jq '.data[] | {id, name: .attributes.name, current_run: .relationships.["current-run"].data}'
tfctl api /organizations/{organization}/workspaces --all --jq '.data[] | select(.attributes.["terraform-version"] | startswith("1.8")) | .attributes.name'
tfctl api /workspaces/{workspace}/vars -p workspace=NAME --jq '.data[] | {key: .attributes.key, category: .attributes.category, sensitive: .attributes.sensitive}'
tfctl run status NAME_OR_ID
tfctl api /organizations/{organization}/workspaces/NAME --jq '.data.relationships.["current-run"].data.id'
tfctl api /workspaces/{workspace}/runs -p workspace=NAME --jq '.data[] | select(.attributes.status == "planned") | {id, status: .attributes.status}'
tfctl api /organizations/{organization}/workspaces --all \
--jq '.data[] | select(.attributes.["vcs-repo"] != null and .attributes.["vcs-repo"].identifier == "org/repo") | {id: .id, name: .attributes.name}'
tfctl api /organizations/{organization}/teams -f 'filter[names]=TEAM_NAME' --jq '.data[0].id'
tfctl api /team-workspaces -f 'filter[team][id]=TEAM_ID' --jq '.data[] | {workspace_id: .relationships.workspace.data.id, access: .attributes.access}'
tfctl api /organizations/{organization} --jq
tfctl api /workspaces/{workspace}/current-state-version -p workspace=NAME --jq
tfctl api /organizations/{organization}/varsets --all --jq
tfctl api /workspaces/{workspace}/configuration-versions -p workspace=NAME --jq
tfctl api /workspaces/{workspace}/notification-configurations -p workspace=NAME --jq
tfctl api /organizations/{organization}/workspaces --all --jq
tfctl api /organizations/{organization}/workspaces --all --jq
tfctl api /workspaces/{workspace}/runs -p workspace=NAME --all --jq
tfctl api /runs/RUN_ID --jq
tfctl api /plans/PLAN_ID --jq
tfctl run start NAME_OR_ID
tfctl api /workspaces/{workspace}/relationships/remote-state-consumers -p workspace=NAME \
-i
tfctl api /workspaces/{workspace}/relationships/varsets -p workspace=NAME \
-X POST -i
tfctl api /runs/{run-id}/policy-checks --jq
tfctl api schema search --json
tfctl api schema get OPERATION_ID
Secret Redaction
By default, tfctl will redact the output of sensitive values from api command output, which includes artifact download URLs, log URLs, tokens, private SSH keys, and some unknown things such as variable values that match a token heuristic. This includes --json and --jq output. When extracting a secret that you need, use the --no-redact global flag to disable redaction for a single request.
Output flags
| Need | Flag |
|---|
| Filter / extract | --jq '<expr>' |
| Full JSON | --json |
| Render for human | --markdown |
| Audit a mutation | --dry-run |
| Don't redact secrets | --no-redact |
--jq implies --json. Don't pass both. Always pass one explicitly — don't rely on auto-detect.
JSON:API conventions
Responses are JSON:API envelopes: {data: {id, type, attributes, relationships}} (or data: [...] for lists). To follow a link, read data.relationships.<name>.data which gives {id, type} or null. A null is final — there is no related resource.
Pagination: default page 1. Add --all for all pages (cap 2000), or --page-size N / --page-number N for explicit control. For counts, use --page-size 1 --jq '.meta.pagination.["total-count"]'.
Mutations & batch updates
tfctl api /workspaces/{workspace}/vars -p workspace=NAME -a key=VARKEY -a value=VALUE -a category=env
tfctl api /workspaces/{workspace}/vars -p workspace=NAME -a key=VAR1 -a value=VAL1 -a category=env
tfctl api /workspaces/{workspace}/vars -p workspace=NAME -a key=VAR2 -a value=VAL2 -a category=env
tfctl api /workspaces/{workspace}/X -X PATCH -p workspace=NAME -i '{"data":{"type":"X","attributes":{…}}}'
Add --dry-run to preview without sending.
Exit codes (quick map)
| Code | Meaning | Action |
|---|
| 0 | Success | Done |
| 1 | Informational message or usage error | Read stderr; if it says "no current run" or similar, that IS the answer — stop |
| 2 | Not found (workspace/run doesn't exist) OR invalid auth | Verify the ID/name is correct, then check token if still failing |
| 3 | Auth token expired or invalid | Re-authenticate |
| 4 | Network error | Retry after brief delay |
| 5 | Rate limited (429) or server error (5xx) | Retry with backoff |
| 6 | Resource has an error state | The error is already diagnosed in output (e.g., plan failed); read it |
Important: When an API returns data: [] (empty list) or data: null, that IS the answer. Don't retry with different flags or endpoints.
Common troubleshooting
| Symptom | Cause | Fix |
|---|
exit 2 when listing workspaces | Organization doesn't exist or auth token has no access | Verify org name and re-authenticate |
exit 1 with "no current run" | Workspace simply has no active run (informational) | This is the answer — stop, don't verify |
exit 3 when making any API call | Auth token expired | Re-authenticate with tfctl login |
exit 5 (429 rate limit) | Too many requests | Wait and retry; tfctl will backoff automatically |
exit 6 with "plan is errored" | Terraform plan had syntax errors (not CLI error) | Read the plan output for details |
Empty list (data: []) when filtering | No resources match criteria | Verify criteria is correct; empty list is valid answer |
Smart defaults
{organization} resolves from active profile if set.
{workspace} resolves from a local cloud {} block in CWD.
- Already-formed IDs (
ws-…, team-…, prj-…, varset-…) are passed through as-is.