Secrets & Credentials
/dashboard/secrets.md. Cloud Harness MCP provides write-only credential storage and automatic injection into remote Docker execution workspaces with defense-in-depth output stream redaction.
Scopes & Precedence
Secrets are partitioned into two distinct scopes:
| Scope | Availability | Injection Trigger | Precedence |
|---|---|---|---|
| Global Secrets | All projects and workspaces owned by the authenticated identity | Active global runtime secrets are automatically inherited by every remote Docker workspace | Base tier |
| Environment Secrets | Scoped to a specific project environment | Injected only when environmentId and confirmEnvironmentInjection: true are both supplied | Overrides global secrets on name collision |
Collision Precedence
When a workspace opens with an environment that contains a secret sharing the exact same key name as a Global Secret, the environment secret takes precedence and overrides the global value for that workspace container.
Local Stdio Limitation
Retained secret discovery and automatic environment injection apply exclusively to remote Docker workspaces. In local stdio mode (--transport stdio), secrets discovery and injection are unsupported.
Write-Only Security Model
- Submission & Encryption: Secrets are created and updated through the authenticated, CSRF-protected Operator Dashboard. Values are encrypted at rest using AES-256-GCM via the runner-held keyring (
SECRET_KEYRING_FILE). - Never Returned: Neither the browser dashboard nor MCP tools ever return secret values or ciphertext. The Dashboard UI and APIs expose non-secret metadata (
id,name,description,state,version,generation,createdAt,updatedAt,deletedAt), while MCPsecrets_listexposes{ name, description, scope, environmentId, version, updatedAt }. - Container Injection: During container provisioning, runtime secrets are briefly written to an ephemeral mode-0600 host environment file and immediately removed after Docker container start.
- Purpose Classification: Secrets carry a
purposeattribute (runtimevsprovisioning). Onlyruntimesecrets are injected into workspace container environments;provisioningsecrets are excluded from runtime container injection.
Ingest-Time Stream Redaction
To prevent accidental leakage of sensitive tokens into LLM context transcripts:
- 4-Byte Minimum Threshold: Secret values meeting the length requirement (≥ 4 UTF-8 bytes) are compiled into the runner's streaming redactor.
- Stream & Result Sanitization: Streaming task, shell, and session stdout/stderr chunks—including matches spanning stream chunk boundaries—are sanitized to
[REDACTED_SECRET: <NAME>]before retained memory buffering. Synchronousexec_runoutputs and error messages are sanitized after capture before return. - Monotonic Offset Preservation: Redaction replaces exact matches without corrupting byte-offset pagination cursors.
- Defense-in-Depth: Stream redaction targets exact raw byte matches. It does not replace encoded (e.g., base64 or hex), hashed, case-transformed, or partial secret derivatives. Repository code and commands must not deliberately print or transform credentials.
Discovering Secrets via MCP (secrets_list)
AI agents can inspect available secret names and descriptions without reading sensitive values:
{
"name": "secrets_list",
"arguments": {
"workspaceId": "ws_aaaaaaaaaaaaaaaaaaaa"
}
}Example Response (structuredContent / data):
{
"secrets": [
{
"name": "NPM_TOKEN",
"description": "Auth token for private npm registry",
"scope": "global",
"environmentId": "global",
"version": 1,
"updatedAt": 1725000000000
},
{
"name": "DATABASE_URL",
"description": "Staging PostgreSQL connection string",
"scope": "environment",
"environmentId": "env_staging_0123456789",
"version": 2,
"updatedAt": 1725050000000
}
]
}GitHub credentials in workspaces
GH_TOKEN and GITHUB_TOKEN are accepted secret names, unlike every other control-plane, GitHub App, and toolchain name. A global runtime secret with one of those names:
- authenticates the workspace's bundled
ghCLI without an interactive login; - serves as the runner's fallback credential for
github_actionand private Git operations (clone, fetch, pull, push) when no GitHub App token is available; and - can be referenced as a
secretReffor MCP gateway headers.
Because runtime secrets are injected into the executor environment, any process in that workspace can read the value. Create one only when you intend the workspace to act as you on GitHub, and prefer the narrowest fine-grained token scoped to the repositories the workspace needs. Egress is the shipped default, so a readable credential is exfiltratable by repository-controlled code; to remove that exposure, switch the instance default on Settings or the individual workspace back to networkProfile: network-none, which blocks all executor egress.
The runner-environment alternative (GH_TOKEN in the deployment environment) authenticates harness-side operations only and is never injected into a workspace. That environment source is ignored in cloudflare-access mode, where the per-principal secret above is used instead.
Names that remain rejected include RUNNER_TOKEN, PATH, HOME, SHELL, SECRET_KEYRING*, STATE_DB, JOBS_ROOT, DOCKER_HOST, and everything under the GITHUB_APP_, RUNNER_, ACCESS_, CF_, CLOUDFLARE_, HARNESS_, CH_, DOCKER_, XDG_, NPM_, UV_, BUN_, PNPM_, GIT_, and LD_ prefixes.
Keyring Rotation
To rotate the underlying AES-256-GCM encryption key without losing access to existing secret versions:
Back up the coherent recovery set (database, artifacts, and configuration).
Add the new active key version to
SECRET_KEYRING_FILEwhile retaining all previous key versions.Restart the runner with the updated keyring file.
Quiesce secret writes and run the runner re-encryption script:
bashnpm run secrets:rekey -w @cloud-harness/runnerVerify completion, take a post-rotation backup, and retain old decrypt keys throughout the rollback window.