---
title: "Variables and secrets"
description: "Versioned workspace Terraform and environment variables with write-only sensitive values."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.stackship.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Variables and secrets

The first release supports variables directly on a workspace. Organization,
project, priority, and attached variable sets are not available in private
beta.

## Categories

| Category | Runner behavior |
| --- | --- |
| `terraform` | Supplied as an OpenTofu input variable |
| `environment` | Supplied as a process environment entry |

Terraform variables also carry an `hcl` flag. Environment values are strings.
Keys are bounded to 256 characters, descriptions to 1,024 characters, and
values to 65,536 bytes. The native, dashboard, and OpenTofu-compatible
variable mutation envelopes are bounded to 131,072 bytes so the maximum value
fits with its JSON metadata. Credential-profile mutation documents use a
separate 100,000-byte envelope limit for their supported 64 KiB canonical
generic-secret map and bounded profile metadata.

Environment variables cannot use the runner-owned `STACKSHIP_*` or `TFC_*`
namespaces, `TF_CLI_ARGS`, any `TF_CLI_ARGS_*` subcommand override, `TF_LOG`,
any `TF_LOG_*` subsystem setting, `TF_FORCE_LOCAL_BACKEND`, or
`TF_TOKEN_stackship_run`. StackShip also reserves OpenTofu controls that can
redirect the managed workspace, data directory, provider process, plugin
cache or transport, encryption, or persistence behavior:
`TF_CLI_CONFIG_FILE`, `TF_CLOUD_HOSTNAME`, `TF_CLOUD_ORGANIZATION`,
`TF_CLOUD_PROJECT`, `TF_DATA_DIR`, `TF_DISABLE_PLUGIN_TLS`, `TF_ENCRYPTION`,
`TF_IN_AUTOMATION`, `TF_PLUGIN_CACHE_DIR`,
`TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE`, `TF_REATTACH_PROVIDERS`,
`TF_STATE_PERSIST_INTERVAL`, and `TF_WORKSPACE`. OpenTofu injects
`TF_CLI_ARGS*` into its command line and its raw logging controls can write
state or credentials to process output or disk, so StackShip reserves both
complete prefix families instead of maintaining a subcommand or subsystem
allowlist. The dashboard and both native and OpenTofu-compatible APIs reject
these names before persisting a variable, with guidance to choose a
provider-specific name. OpenTofu input variables remain available because
StackShip projects them into the separate `TF_VAR_*` namespace at execution
time.

## Lifecycle

The native v1 API and dashboard support:

- list current workspace variable metadata;
- create a key and its first value version;
- replace the value with a new immutable version, or edit the description
  and HCL flag while keeping the stored value; and
- delete the variable.

Create, replace, and delete require an idempotency key. An exact replay returns
the durable result; a changed request under the same key is rejected.

The current list response includes variable id, current version id, category,
HCL flag, key, description, sensitivity, value digest, and timestamps.

In the dashboard, each row shows the key, a category badge using the same
labels as the create form ("OpenTofu input" and "Environment"), an HCL badge
when the flag is set, a Write-only badge for sensitive values, and the value
digest truncated with the full SHA-256 as a tooltip. The edit form sits
behind a per-row **Edit** action, and at most one edit form is open at a
time. Its value field is optional and labeled "New value (leave blank to
keep the current value)": submitting without a value updates the
description and the OpenTofu-input HCL flag while the stored value — even a
sensitive one — stays untouched. Sensitive values remain write-only and are
never prefilled into the form. Changing the sensitive flag requires
re-entering the value; the dashboard asks for it inline before any request
is sent, and the API enforces the same rule with
`400 WORKSPACE_VARIABLE_VALUE_REQUIRED`.

After a successful dashboard save, the variable row announces either that a
new immutable version was created or that the metadata was updated with the
stored value unchanged, and keeps that confirmation visible while the
updated metadata is rendered. Any submitted value is cleared before the
confirmation is shown; sensitive plaintext never appears in the message.

The create card also offers **Add several at once**: paste `KEY=value`
lines to create non-sensitive variables of the selected category, one
idempotent create per line. Blank lines and `#` comments are skipped;
malformed lines are reported by line number before any request is sent, and
a partial failure reports how many variables were created. Members without
`workspace:admin` see a quiet read-only notice above the list.

## Sensitive values

When `sensitive` is true:

- the API encrypts the value before database persistence;
- a random per-version AES-256-GCM data key is wrapped by the internal
  credential broker;
- authenticated additional data binds the organization, workspace, variable,
  version, and category;
- the database stores ciphertext, nonce, wrapped key, key version, and digest;
- list and mutation responses return `value: null`; and
- replacing the secret creates a new immutable version rather than changing
  old ciphertext.

The dashboard clears the submitted sensitive value from component state after
the request settles. There is no product endpoint for reading sensitive
plaintext.

Non-sensitive variables may return their current value. Their digest is still
recorded so an immutable run input can bind the value it used.

## Run snapshots

A managed run resolves the current variable versions before execution and
stores version ids and digests in its immutable input snapshot. Non-sensitive
values may be carried in that snapshot. Sensitive values appear only as secret
version references for the credential broker.

Speculative runs omit sensitive workspace-variable references. Manifest
publication fails closed if a speculative snapshot contains one.

## Generic secret credential profiles

Use a `generic_secret` credential profile when a plan or apply must receive a
customer-managed token, password, or other environment value for which no
federation flow exists. Profile versions accept between 1 and 32 unique
environment entries. Their canonical UTF-8 JSON representation may be at most
64 KiB. Values cannot contain a NUL code point because operating-system
process environments cannot represent it. The API rejects such a value before
sealing, and the credential broker and runner enforce the same boundary again
before decryption output can reach process creation.

Plan and apply sandboxes have unrestricted public outbound network access, so
every generic secret injected into a run is reachable by customer
configuration, providers, data sources, modules, and processes such as
`local-exec`. StackShip does not infer or restrict a destination from an
environment name or secret value. Trust the repository and its dependencies,
scope each secret to the minimum authority and lifetime, and prefer
provider-native short-lived federation when it is available.

Names must match `[A-Za-z_][A-Za-z0-9_]{0,127}`. StackShip rejects process
control names such as `PATH`, `HOME`, `NODE_OPTIONS`, `LD_PRELOAD`, OpenTofu
control names such as `TF_CLI_CONFIG_FILE`; command-line overrides
`TF_CLI_ARGS` and `TF_CLI_ARGS_*`; raw logging settings `TF_LOG` and
`TF_LOG_*`; the managed-target, data, provider, plugin, encryption, and
persistence controls listed above; and the reserved `STACKSHIP_*` and `TFC_*`
namespaces. Publication and the runner independently enforce the same
reserved-name contract, and also reject any collision with a workspace
environment variable, a `TF_VAR_*` projection, another credential, or another
runner-owned name.

The dashboard validates reserved names before submission. After a rejected
generic-secret request, it clears every plaintext value while preserving the
non-secret environment names and risk acknowledgements so the operator can
correct the request and re-enter only the secrets. A successful request clears
the complete form.

The value map is write-only:

- the API sends it directly to the internal credential broker for sealing;
- the broker encrypts it with a random AES-256-GCM data key;
- the broker wraps that data key with the active, broker-only platform key;
- authenticated context binds the envelope to the exact organization,
  workspace, profile-version id, and sorted environment-name set; and
- PlanetScale stores only the ciphertext, nonce, wrapped key, key version, and
  a keyed digest derived from the random data key.

Equal plaintext in two versions does not produce the same digest. The API also
reduces the secret-bearing part of an idempotent create request to a
domain-separated HMAC before the ordinary request fingerprint reaches
PlanetScale. A database reader therefore cannot use either field to test
plaintext guesses without a key held outside the database.

List, create, and verification responses return `configuration: null` plus the
safe environment-name metadata. There is no plaintext read endpoint. A
version remains pending until the broker successfully decrypts and validates
its stored envelope; only then can it become current. A profile with no
verified current version is a detached draft: it is visible to operators but
does not participate in run snapshots. Once a current version exists, run
snapshotting fails closed unless that version remains verified and valid for
the requested run.

New generic secret versions use `plan_and_apply`. They are never valid for
`plan`, `apply`, or `speculative_plan`, including a same-repository pull
request. New cloud federation versions use `plan_and_apply`, or
`speculative_plan` when explicitly selected for pull-request plans. A run
snapshot contains only the selected credential and profile-version ids. The
execution manifest adds only the environment names, expiry, and an opaque
one-time handle.

## Archive and revoke

Archiving a credential profile detaches it from its workspace without deleting
the profile, its immutable versions, or audit history. Revoking an exact
version marks it `disabled`, records when it was disabled, and removes its
current marker. The API can revoke a current or historical version for
incident response. The dashboard presents the first revoke control only on the
current verified version and requires a separate destructive confirmation for
both revoke and archive.

In the dashboard, credential profiles across every workspace appear in one
filterable table with spelled-out provider names, and a profile page opens
from its profile ID alone — when a link omits the workspace, StackShip
resolves the owning workspace server-side. The profile page leads with
version status and verification; the provider trust template sits beside the
version awaiting verification with a copy control; and the archive control
lives in a separate confirmation-guarded section at the end of the page.

These transitions take effect at use time, not only when a run is created.
StackShip re-resolves an exact pinned credential before manifest issuance and
again before runner redemption. A revoked version or archived profile
therefore cannot enter a new snapshot and cannot be redeemed by an already
queued run. The run receives no replacement credential automatically.

Both actions require `credential:write` and append an organization audit event.
They never erase a sealed generic-secret envelope or expose its plaintext.

Before the dashboard submits a generic secret version, the operator must
separately acknowledge that plaintext enters the selected runner sandbox,
infrastructure code and providers can access it, log redaction cannot make
malicious infrastructure configuration safe, and federation is preferred
whenever it is available. The API remains authoritative for the write-only
shape and phase restrictions; dashboard acknowledgements do not make
untrusted infrastructure configuration safe.

The handle is redeemable only by the authenticated runner for the exact
organization, workspace, run, attempt, phase, sandbox, input digest,
credential, profile version, environment-name set, and expiry used at issue
time. A mismatched redemption does not consume it; one exact successful
redemption deletes it atomically. The envelope is fetched from PlanetScale and
presented only to the broker during redemption. It is never placed in a
Cloudflare Workflow argument, Durable Object handle record, sandbox launch
request, GitHub payload, policy input, audit detail, analytics event, or log.

## Prefer federation

Use AWS, Azure, or Google Cloud federation profiles for cloud access whenever
possible. Federation issues short-lived, phase-bound identity material and
does not place a long-lived provider secret into the run environment.

The first release supports the commercial AWS partition. AWS GovCloud and AWS
China profiles are rejected rather than being issued through an incompatible
global STS or trust contract.

## Related pages

- [Managed execution](/runs/runners)
- [GitHub source connections](/workspaces/vcs)
- [v1 REST API](/automation/api)

Source: https://docs.stackship.run/workspaces/variables/index.mdx
