VAULTS
A vault is a named secret storage backend. Vaults store and retrieve sensitive
values — API keys, passwords, tokens — used in model definitions and workflows.
Vault configurations live in vaults/{vault-type}/{id}.yaml within a Swamp
repository. Encrypted secret data lives in .swamp/secrets/.
File Structure
vaults/
local_encryption/
0dd4c691-0b7b-4fae-9741-31e96a4efc69.yaml
.swamp/secrets/
local_encryption/
my-secrets/
.key
api-key.enc
db-password.encThe vaults/ directory stores vault configurations (tracked in git). The
.swamp/secrets/ directory stores encrypted secret data (not tracked in git,
not included in datastore sync — see
Local Storage and Datastore Sync below).
Local Storage and Datastore Sync
local_encryption vault secrets always live in .swamp/secrets/ on the local
machine. They are never included in datastore migration or ongoing sync,
regardless of the datastore backend (filesystem, S3, GCS, or any other
extension-provided backend).
This exclusion enables a shared workflow, per-user local credentials
pattern: a vault's definition (vaults/local_encryption/*.yaml) is
git-tracked and shared across teammates, but each person's actual secret values
stay local to their machine even when the rest of the repository's state syncs
through a shared datastore. Each teammate runs swamp vault put to store their
own credentials locally.
When a team needs shared access to the same secret values, migrate the vault to a cloud backend such as AWS Secrets Manager, Azure Key Vault, or 1Password — see Vault Migration and the Collaborate guide.
Vault Configuration
Each vault is defined by a YAML file with the following fields:
id: 0dd4c691-0b7b-4fae-9741-31e96a4efc69
name: my-secrets
type: local_encryption
config:
auto_generate: true
base_dir: /home/user/my-repo
createdAt: "2026-04-07T16:59:59.039Z"id
Unique identifier for the vault configuration.
| Property | Value |
|---|---|
| Type | string (UUID v4) |
| Required | Yes |
| Default | Auto-generated on create |
| Format | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
name
Human-readable name used to reference the vault in CLI commands and CEL expressions.
| Property | Value |
|---|---|
| Type | string |
| Required | Yes |
| Default | None |
Must be unique within the repository. Must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.
type
Vault provider type.
| Property | Value |
|---|---|
| Type | string |
| Required | Yes |
| Default | None |
See Vault Types for available values.
config
Provider-specific configuration.
| Property | Value |
|---|---|
| Type | Record<string, unknown> |
| Required | No |
| Default | {} |
Available keys depend on the vault type. See the configuration section for each type below.
createdAt
ISO 8601 timestamp of when the vault was created.
| Property | Value |
|---|---|
| Type | string |
| Required | Yes |
| Default | Set on create |
| Format | ISO 8601 (Z) |
auditReads
Enables read-operation auditing for the vault. When true, secret reads are
recorded in the vault audit trail alongside write operations. Write operations
(put, delete, annotate) are always audited when an audit repository
exists, regardless of this setting.
| Property | Value |
|---|---|
| Type | boolean |
| Required | No |
| Default | false |
| Set via | --audit-reads on create, or edit the vault YAML directly |
Vault Types
local_encryption
Stores encrypted secrets in local files using AES-GCM encryption. This is the built-in vault type available in every Swamp repository.
Configuration
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
ssh_key_path |
string | No | ~/.ssh/id_rsa |
Path to an unencrypted SSH private key |
auto_generate |
boolean | No | true |
Auto-generate an encryption key if no SSH key |
key_file |
string | No | secrets/.key |
Custom path for the auto-generated key file |
base_dir |
string | No | Repository root | Base directory for secret storage (auto-injected) |
When no ssh_key_path is provided and auto_generate is true (the default),
Swamp creates a random 72-character key (two concatenated UUIDs) at
.swamp/secrets/local_encryption/{vault-name}/.key.
When ssh_key_path is provided, the SSH private key is used as key material for
PBKDF2 derivation. The key must be unencrypted — passphrase-protected SSH keys
(both legacy PEM and OpenSSH format) are rejected.
# Auto-generated key (default)
$ swamp vault create local_encryption my-secrets
# SSH key-backed
$ swamp vault create local_encryption infra \
--config '{"ssh_key_path":"~/.ssh/id_ed25519"}'Note
The key-source fields ssh_key_path, auto_generate, key_file and
base_dir can only be chosen by a local invocation, run on the host that
stores the secrets. Over --server, vault create and vault migrate use
the server's defaults (auto_generate: true, base_dir set to the serve
repository) and refuse any other value; vault edit must keep the stored
values. A remote create fails like this:
Cannot set ssh_key_path for vault 'infra': a local_encryption vault created remotely uses the server's key source. Leave these fields out, or run the command on the host running swamp.Encryption Details
| Property | Value |
|---|---|
| Algorithm | AES-GCM (authenticated encryption) |
| Key size | 256-bit |
| IV size | 96-bit (random per secret) |
| Salt size | 128-bit (random per secret) |
| Key derivation | PBKDF2 with SHA-256, 100,000 iterations |
Each encrypted file is stored as JSON:
{
"iv": "ZhURGq+UA75P0Ydb",
"data": "OQayYKhVm6AcQXpzukNcSueagt1b3OQvtxg9it8=",
"salt": "sCvS1h08k+lFZ37PIfjSLQ==",
"version": 2
}All values are base64-encoded. version tracks the encrypted data format.
File Permissions
| Path | Mode |
|---|---|
.swamp/secrets/local_encryption/{vault-name}/ |
700 |
.swamp/secrets/local_encryption/{vault-name}/.key |
600 |
.swamp/secrets/local_encryption/{vault-name}/{key}.enc |
600 |
SSH key files are validated for 600 or stricter permissions on Unix systems.
Vaults can be migrated between types using swamp vault migrate. The vault name
is preserved, so all existing vault.get() references continue to resolve
without modification. See Vault Migration and the
swamp vault migrate CLI command for
details.
Extension Vault Types
Additional vault types are available through extensions. Every Swamp command
needs a swamp-club.com account; creating a vault with an extension type
additionally requires a collective token to carry the vault:* scope.
local_encryption needs no scope.
| Type | Extension | Description |
|---|---|---|
@swamp/aws-sm |
@swamp/aws-sm |
AWS Secrets Manager |
@swamp/azure-kv |
@swamp/azure-kv |
Azure Key Vault |
@swamp/1password |
@swamp/1password |
1Password integration |
Legacy type names (aws, aws-sm, azure, azure-kv, 1password) are
automatically mapped to their scoped equivalents.
Custom Vault Types
Custom vault providers can be added via extension files in
extensions/vaults/*.ts. Each file must export a vault object implementing
the Vault Provider interface. See
the Create and Publish guide for
packaging extensions.
Custom vault type names must use the @collective/name format. Reserved
collectives (swamp, si) are not allowed.
$ swamp vault type search{
"query": "",
"results": [
{
"type": "local_encryption",
"name": "Local Encryption",
"description": "Store encrypted secrets in local files using AES-GCM encryption. Uses SSH private key or auto-generated key for encryption."
}
]
}Key Storage and Retrieval
Storing Secrets
$ swamp vault put my-secrets api-key
Enter value for api-key: ********17:00:04 INF vault·put Stored secret "api-key" in vault "my-secrets"The value can be provided in several ways:
| Method | Command |
|---|---|
| Interactive | swamp vault put <vault> <key> (hidden prompt) |
| Piped stdin | echo "$SECRET" | swamp vault put <vault> <key> |
| Third argument | swamp vault put <vault> <key> <value> |
| Inline | swamp vault put <vault> KEY=VALUE |
Use --yes (-y) to overwrite an existing key without confirmation.
Piping via stdin is recommended for scripts and CI. The third-argument and
inline KEY=VALUE forms expose the secret in shell history, process tables, and
system logs — use them only for non-sensitive test data.
Metadata labels can be attached at store time with --label (repeatable).
Labels are stored as annotation labels — the same as
vault annotate --label — and are visible via vault inspect.
$ swamp vault put my-secrets api-key --label owner=platform-team --label env=prod
Enter value for api-key: ********17:00:04 INF vault·put Stored secret "api-key" in vault "my-secrets"Retrieving Secrets
Secrets can be retrieved in two ways:
- CEL expressions in model definitions and workflows — see CEL Integration below.
- CLI via
swamp vault read-secret— reads a single secret value, with a confirmation prompt for safety.
Listing Keys
$ swamp vault list-keys my-secrets17:00:29 INF vault·list-keys Vault "my-secrets" ("local_encryption"): 2 key(s)
17:00:29 INF vault·list-keys - "api-key"
17:00:29 INF vault·list-keys - "db-password"Lists key names only. Values are never displayed.
Annotations
Annotations attach provenance metadata to a vault secret — a source URL, free-text notes, and key=value labels. Annotations are stored alongside the secret and do not affect the secret value itself.
Fields
| Field | Type | Description |
|---|---|---|
url |
string (optional) |
URL associated with this secret |
notes |
string (optional) |
Free-text notes about this secret |
labels |
Record<string, string> |
Key=value metadata labels |
updatedAt |
string (ISO 8601) |
Timestamp of the last annotation update |
Merge Semantics
Annotations use merge semantics: only the fields you specify are updated,
existing fields are preserved. For example, updating notes leaves url and
labels unchanged. Labels are merged additively — new labels are added to
existing ones.
Use --clear to remove all annotations from a secret.
Annotating Secrets
$ swamp vault annotate my-secrets api-key \
--url https://console.aws.com/iam \
--notes "Production API key" \
--label env=prod --label team=infraINF vault·annotate Annotated "api-key" in vault "my-secrets" (fields: "url, notes, labels")Inspecting Metadata
$ swamp vault inspect my-secrets api-keyINF vault·inspect Metadata for "api-key" in vault "my-secrets":
INF vault·inspect size: 42 chars (42 bytes)
INF vault·inspect url: "https://console.aws.com/iam"
INF vault·inspect notes: "Production API key"
INF vault·inspect label: "env"="prod"
INF vault·inspect label: "team"="infra"
INF vault·inspect updated: "2026-05-22T23:53:23.906Z"
INF vault·inspect refresh-hook: (none)JSON output:
$ swamp vault inspect my-secrets api-key --json{
"vaultName": "my-secrets",
"secretKey": "api-key",
"vaultType": "local_encryption",
"sizeBytes": 42,
"sizeChars": 42,
"valueType": "string",
"supportsAnnotations": true,
"hasAnnotation": true,
"annotation": {
"updatedAt": "2026-05-22T23:53:23.906Z",
"url": "https://console.aws.com/iam",
"notes": "Production API key",
"labels": {
"env": "prod",
"team": "infra"
}
},
"supportsRefreshHooks": true,
"hasRefreshHook": false,
"refreshHook": null
}When no annotation exists:
$ swamp vault inspect my-secrets db-passwordINF vault·inspect Metadata for "db-password" in vault "my-secrets":
INF vault·inspect size: 12 chars (12 bytes)
INF vault·inspect annotation: (none)
INF vault·inspect refresh-hook: (none)Provider Support
Annotation support is opt-in per vault provider. Providers that implement the
VaultAnnotationProvider
interface support annotations. The built-in local_encryption provider and the
official extension providers (@swamp/aws-sm, @swamp/azure-kv,
@swamp/1password) support annotations. Custom vault providers may or may not —
check the extension's documentation. If a provider does not support annotations,
swamp vault annotate returns an error.
Refresh Hooks
A refresh hook is a shell command attached to a secret that re-fetches the
secret value when it becomes stale. Refresh hooks are configured per-secret
using flags on swamp vault put.
Configuration
| Flag | Description |
|---|---|
--refresh-from |
Shell command to run to produce a new secret value (stdout is captured) |
--refresh-ttl |
Duration before the secret is considered stale (e.g. 30s, 50m, 1h) |
--clear-refresh |
Remove the refresh hook from this secret |
Both --refresh-from and --refresh-ttl are required together. Providing one
without the other is an error.
$ swamp vault put my-vault GCP_TOKEN \
--refresh-from "gcloud auth print-access-token" \
--refresh-ttl 50m
Enter value for GCP_TOKEN: ********INF vault·put Stored secret "GCP_TOKEN" in vault "my-vault"To remove a refresh hook from a secret:
$ swamp vault put my-vault GCP_TOKEN --clear-refresh --yesINF vault·put Stored secret "GCP_TOKEN" in vault "my-vault"Refresh-on-Read Behavior
When vault.get() or swamp vault read-secret accesses a secret with a refresh
hook, the vault checks whether the TTL has elapsed since the last refresh. If
the secret is stale, the refresh command runs and the returned value replaces
the stored secret.
INF vaults Refreshed secret "GCP_TOKEN" in vault "my-vault"Failure Semantics
When the refresh command fails (non-zero exit code), the stale value is returned and a warning is logged. The secret is not updated.
WRN vaults Refresh command failed for "GCP_TOKEN" in vault "my-vault": "". Returning stale value.Inspecting Refresh Hooks
swamp vault inspect shows refresh hook configuration alongside annotations.
$ swamp vault inspect my-vault GCP_TOKENINF vault·inspect Metadata for "GCP_TOKEN" in vault "my-vault":
INF vault·inspect size: 13 chars (13 bytes)
INF vault·inspect annotation: (none)
INF vault·inspect refresh:
INF vault·inspect command: "echo refreshed-token"
INF vault·inspect ttl: "50m"
INF vault·inspect last refreshed: neverAfter a successful refresh:
INF vault·inspect Metadata for "GCP_TOKEN" in vault "my-vault":
INF vault·inspect size: 15 chars (15 bytes)
INF vault·inspect annotation: (none)
INF vault·inspect refresh:
INF vault·inspect command: "echo refreshed-token"
INF vault·inspect ttl: "1s"
INF vault·inspect last refreshed: "2026-06-08T21:41:01.488Z"JSON output includes size, capability, and refresh hook fields:
$ swamp vault inspect my-vault GCP_TOKEN --json{
"vaultName": "my-vault",
"secretKey": "GCP_TOKEN",
"vaultType": "local_encryption",
"sizeBytes": 13,
"sizeChars": 13,
"valueType": "string",
"supportsAnnotations": true,
"hasAnnotation": false,
"annotation": null,
"supportsRefreshHooks": true,
"hasRefreshHook": true,
"refreshHook": {
"command": "echo refreshed-token",
"ttlMs": 3000000,
"ttl": "50m",
"lastRefreshedAt": null
}
}When no refresh hook is configured, hasRefreshHook is false and
refreshHook is null.
CEL Integration
Vault secrets are accessed in CEL expressions using vault.get() and
vault.put().
vault.get(vault_name, key)
Retrieves a secret value from the named vault.
globalArguments:
secret: "${{ vault.get('my-secrets', 'api-key') }}"Supported quoting styles for vault name and key arguments:
# Single quotes
secret: "${{ vault.get('my-secrets', 'api-key') }}"
# Double quotes
secret: "${{ vault.get(\"my-secrets\", \"api-key\") }}"
# Backticks
secret: "${{ vault.get(`my-secrets`, `api-key`) }}"
# Unquoted (when names contain only alphanumeric characters and hyphens)
secret: "${{ vault.get(my-secrets, api-key) }}"vault.put(vault_name, key, value)
Stores a value in the named vault. Used in expressions where a computed value needs to be persisted as a secret.
globalArguments:
store: "${{ vault.put('my-secrets', 'generated-key', self.data.attributes.token) }}"Expression Context
vault.get() expressions are deferred to runtime — they are not evaluated
during definition validation or persistence. This means the vault and key must
exist when the model method or workflow step executes.
The vault context variable is available in:
globalArgumentsvalues in model definitionsmethods.*.argumentsvalues in model definitions- Step task
inputsin workflows forEach.inexpressions in workflows
See the CEL Expressions reference for the full expression language.
Secrets and Inputs
A vault.get() value is redacted as *** in step data and run logs, and
evaluated definitions keep the expression rather than the value. Pass a vault
reference through an input, the environment, or stdin, never the secret value:
| What is passed | Secret persisted in cleartext |
|---|---|
A secret value via swamp workflow run --input, --input-file, --stdin |
Yes: run record, history index, child runs, evaluated definitions, data of models that record arguments |
A secret value via swamp workflow resume --input, --input-file, --stdin |
Yes: the same places once merged; the run record's resumeInputs lists only key names |
A secret value via ${{ env.* }} |
Yes: data of models that record arguments (not redacted) |
${{ vault.get(...) }} via swamp model method run --input, --input-file, --stdin |
No: resolved at run time and redacted |
A vault or key name via ${{ env.* }}, as in vault.get(prod-secrets, env.KEY_NAME) |
No |
A vault or key name via a workflow input, as in vault.get(inputs.vaultName, inputs.secretKey) |
No |
A ${{ vault.get(...) }} passed as a swamp workflow run or resume input
value is not evaluated; it reaches the step as literal text (see
Where Expressions Are Evaluated).
Pass the vault name and key as inputs and resolve with
vault.get(inputs.vaultName, inputs.secretKey). See
Input values are inert and recorded.
If a secret has been passed through an input, rotate it. swamp run gc removes
finished runs older than its cutoff (never suspended or running ones), and
swamp workflow delete removes a workflow's runs. Neither removes evaluated
definitions under .swamp/definitions-evaluated/ or model data; remove those
with swamp data delete.
Model Method Run Inputs Are Evaluated
swamp model method run treats its --input, --input-file, and --stdin
values as operator-written. A ${{ vault.get(...) }} or ${{ env.* }} inside
them is resolved, and the method receives the secret in plaintext. Vault values
are redacted in stored output; env values are not. Never pass untrusted text (an
issue body, a ticket, a scraped page) as a model method run input. Pass it as a
workflow input or as model data, which are not evaluated, or remove ${{ from
it first. See
Where Expressions Are Evaluated.
Environment Variable Mounting
When a command/shell step's arguments contain a secret, Swamp passes it to the
executing process as an environment variable rather than in the command string.
This applies to two sources:
vault.get()expressions.- Sensitive fields read through
data.latest(),steps.<name>.outputsormodel.<name>.resource.
Inside an expression a sensitive field is plaintext, so guards, comparisons and
size() see the real value. Only the result spliced into the command is
replaced.
Resolution Process
- During expression evaluation, each
vault.get()call, and each sensitive field value inside an expression's result, is replaced with an internal sentinel token. - Before shell execution, each sentinel is replaced with a shell variable
reference (
${__SWAMP_VAULT_N}), whereNis a sequential index. - The raw secret values are passed as environment variables to the process.
- The shell expands the variables after command parsing, so metacharacters in secret values are never interpreted as shell syntax.
Quoting Behavior
Sentinel replacement is quoting-aware. Each occurrence of a vault.get()
reference takes its form from the quote context it sits in:
| Context | Replacement |
|---|---|
| Inside double quotes | ${__SWAMP_VAULT_N} |
| Outside double quotes | "${__SWAMP_VAULT_N}" |
For example, given a model argument:
methods:
execute:
arguments:
run: "curl -H \"Authorization: Bearer ${{ vault.get('my-secrets', 'api-key') }}\" https://api.example.com"The shell receives:
curl -H "Authorization: Bearer ${__SWAMP_VAULT_0}" https://api.example.com
# with env: __SWAMP_VAULT_0=sk-test-12345A secret containing shell metacharacters like pass;rm -rf / is treated as
literal data because variable expansion happens after command parsing.
A secret used more than once is placed at each use, and every use shares one variable:
methods:
execute:
arguments:
run: |
echo 'literal ${{ vault.get('secrets', 'tok') }}'
printf '[%s]\n' "${{ vault.get('secrets', 'tok') }}"
printf '[%s]\n' ${{ vault.get('secrets', 'tok') }}The shell receives:
echo 'literal "${__SWAMP_VAULT_0}"'
printf '[%s]\n' "${__SWAMP_VAULT_0}"
printf '[%s]\n' "${__SWAMP_VAULT_0}"The shell does not expand a reference inside single quotes, so a single-quoted use prints the literal reference. If any use is single-quoted, the step warns:
Warning Vault secret expression inside single quotes will not be expanded by the shell — use double quotes insteadIn PowerShell, placement is per occurrence too: a vault.get() reference
becomes $env:__SWAMP_VAULT_N inside double quotes and "$env:__SWAMP_VAULT_N"
elsewhere.
Sensitive Field Placement
A sensitive field value is placed per occurrence, by the shell context it sits in. A value that appears several times shares one variable.
| Context | Replacement |
|---|---|
| Unquoted | "${__SWAMP_VAULT_N}" |
| Inside double quotes | ${__SWAMP_VAULT_N} |
Here-document body, unquoted delimiter (<<EOF) |
${__SWAMP_VAULT_N} |
| Inside single quotes | '"${__SWAMP_VAULT_N}"' (closes and reopens) |
Inside ANSI-C quotes ($'...') |
'"${__SWAMP_VAULT_N}"$' (reopens as $'...') |
Here-document body, quoted delimiter (<<'EOF', <<"EOF", <<\EOF) |
The value itself, with a warning |
An unquoted value is wrapped in double quotes, so it is not word-split or glob-expanded.
Given a definition that reads a sensitive token field in each context:
methods:
execute:
arguments:
run: |
a=${{ data.latest('writer', 'main').attributes.token }}
b="x ${{ data.latest('writer', 'main').attributes.token }}"
c='x ${{ data.latest('writer', 'main').attributes.token }}'
d=$'x ${{ data.latest('writer', 'main').attributes.token }}\t'
cat <<EOF
${{ data.latest('writer', 'main').attributes.token }}
EOFThe shell receives:
a="${__SWAMP_VAULT_0}"
b="x ${__SWAMP_VAULT_0}"
c='x '"${__SWAMP_VAULT_0}"''
d=$'x '"${__SWAMP_VAULT_0}"$'\t'
cat <<EOF
${__SWAMP_VAULT_0}
EOFNo environment reference expands inside a quoted-delimiter here-document, so the
value stays in the command line and the step emits a
sensitive_value_in_command_line warning:
Warning Sensitive data value inside a quoted here-document or a single-quoted PowerShell string is passed on the command line — use an unquoted here-document delimiter or double quotes insteadIn PowerShell a sensitive field value becomes $env:__SWAMP_VAULT_N inside
double quotes and "$env:__SWAMP_VAULT_N" elsewhere. Inside a single-quoted
string it stays in the command line, with the same warning.
Limits
- The variable keeps the value out of the command line Swamp spawns. A program
the command starts with the value as an argument (
sh -c,ssh) receives it in its own command line. - Sensitive field values shorter than three characters are not tracked, as with output redaction, and reach the command line as written.
- A step dispatched to a remote worker is sent resolved arguments, so the value is in the command line on the worker, for both sources.
- A
manual_approvalprompt that reads a sensitive field shows the value to the approver.
Sensitive Field Processing
Model types can mark resource output fields as sensitive. When a method produces
sensitive output, the values are automatically stored in a vault and replaced
with vault.get() reference expressions before persistence.
Vault Resolution Order
The vault used for storing sensitive fields is resolved in this order:
- Field-level
vaultNamefrom schema metadata (set by extension authors via.meta({ sensitive: true, vaultName: "..." })in TypeScript) - Spec-level
vaultNamefrom theResourceOutputSpec— set by extension authors in TypeScript, or overridden per-definition via theresources.<specName>.vaultNamefield in the definition YAML - Repo-level
defaultVaultfrom.swamp.yaml— applies to model execution,swamp serveOAuth token storage, and device authorization token storage - First available vault from the vault service (backwards-compatible fallback)
Each level takes precedence over the levels below it. If sensitive fields exist but no vault is resolved at any level, an error is thrown.
Sensitive Method Arguments
Sensitive method arguments (parameters marked as sensitive in the model type
schema) are the user's responsibility to vault. The runtime enforces that
sensitive argument values must be ${{ vault.get(...) }} expressions, but does
not automatically route argument values into a vault — unlike sensitive resource
output fields, which are stored automatically.
Pre-flight Vault Validation
Before a mutating method executes, Swamp checks whether the model has sensitive resource output fields. If it does and no vault is configured, the method is rejected immediately — before any method logic runs.
Error: Model "my-keypair" has sensitive resource output fields but no vault is
configured. Create a vault before running this method:
swamp vault create <type> <name>This is distinct from the persist-time error that occurs during data writing. The pre-flight check prevents the method from starting at all, avoiding partial execution that would fail when attempting to store sensitive output.
Use swamp doctor vaults to scan all
definitions for this condition before running methods.
Auto-Generated Vault Keys
When no custom vault key is specified, the key is generated from the model context:
{sanitized-model-type}-{model-id}-{method-name}-{field-path}Sanitization rules: @ is removed, / and \ are replaced with -.
Example: model type @user/aws/ec2-keypair, field KeyMaterial becomes
user-aws-ec2-keypair-{id}-createKeyPair-KeyMaterial.
Persisted Format
After processing, the persisted data contains vault references instead of raw values:
keyMaterial: "${{ vault.get('my-secrets', 'user-aws-ec2-keypair-abc-createKeyPair-KeyMaterial') }}"Non-string values are JSON-stringified before vault storage.
Values Read Through Expressions
A sensitive field read through data.latest(), steps.<name>.outputs or
model.<name>.resource resolves to its plaintext value inside the expression.
Swamp tracks each resolved value with the vault and key it came from, and never
writes it in plaintext:
| Where the value lands | What is stored or shown |
|---|---|
A command/shell step's command |
An environment variable; see Environment Variable Mounting |
| Evaluated definition and workflow caches, run records and their index, deferred binding snapshots, auto-created definitions | The ${{ vault.get('<vault>', '<key>') }} reference |
swamp model evaluate and swamp workflow evaluate output, report arguments, completed-event arguments |
*** |
| Tags, labels and data-name suffixes | sensitive-<vault>.<key> |
forEach step names |
sensitive-<index>, as in each-sensitive-0 |
Each file that holds such a reference lists it in writtenReferences and
carries a sensitiveFormat marker. An evaluated definition cache looks like:
methods:
execute:
arguments:
run: >
a=${{ vault.get('secrets',
'demo-secret-writer-8f0f6531-3341-4437-adcd-7c1d086355d8-write-creds-main-token')
}}
authoredExpressions:
- '${{ data.latest(''writer'', ''main'').attributes.token }}'
writtenReferences:
- path:
- methods
- execute
- arguments
- run
occurrence: 0
vaultName: secrets
key: >-
demo-secret-writer-8f0f6531-3341-4437-adcd-7c1d086355d8-write-creds-main-token
encoding: raw
dataOrigin: false
sensitiveFormat: 1--last-evaluated and swamp workflow resume restore only the references
listed in writtenReferences. Reference-shaped text that arrives as data is
left as text.
Because tags and labels hold a placeholder, a tag filter on a sensitive value does not match. An auto-created definition holds the reference, so a later run reads the vault directly.
--last-evaluated refuses a cache with no sensitiveFormat marker whose
expressions read data.* or steps.*, since an older Swamp wrote such caches
with the value in plaintext. swamp model method run reports it with the
evaluated_cache_may_hold_secrets error code:
Error: The evaluated definition cached for "consume" was written by an older swamp and may hold sensitive values in plaintext, so it is not replayed. Run the method without --last-evaluated to evaluate it again; that rewrites the cache.Running once without --last-evaluated rewrites the cache in the current
format.
Vault Migration
The swamp vault migrate command migrates a vault to a different backend type
in-place. The vault name stays the same, so all existing vault.get() and
vault.put() expressions continue to work without modification.
How It Works
- Lists all secret keys in the source vault
- Copies each secret value from the current backend to a new provider instance
- Updates the vault configuration file to point to the new backend type (new config is written before the old one is removed)
- The vault name is preserved — all existing
vault.get('name', 'key')expressions resolve identically after migration
Behavior
| Condition | Result |
|---|---|
| Source and target type match | Rejected with an error |
| Target type is a legacy name | Rejected with a message naming the scoped equivalent (e.g. @swamp/aws-sm) |
| Copy fails mid-migration | Source vault is unchanged; no configuration is swapped |
| Config delete fails after swap | Orphaned config file remains; vault operates on the new backend |
--dry-run flag |
Reports secret count and type change; makes no changes |
| Required config field missing | Named with a re-run example, or prompted for in a terminal (details) |
Secrets are copied to the target backend before the configuration file is updated. The new configuration file is written before the old one is removed.
CLI Commands
swamp vault create <type> [name]
Create a new vault configuration. Like every Swamp command, it needs a
swamp-club.com account. Non-local vault types (extension-provided) additionally
require a collective token to carry the vault:* scope. local_encryption
needs no scope.
| Option | Description |
|---|---|
--config |
Provider configuration as JSON. Missing required fields are named, or prompted for in a terminal — see Missing config fields |
--audit-reads |
Enable read-access audit trail for this vault |
--repo-dir |
Repository directory (default .) |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault create local_encryption my-secrets17:00:00 INF vault·create Created vault: "my-secrets" ("Local Encryption")$ swamp vault create local_encryption infra \
--config '{"ssh_key_path":"~/.ssh/id_ed25519"}'17:00:36 INF vault·create Created vault: "infra" ("Local Encryption")ssh_key_path and the other key-source fields can only be
set by a local invocation. With --server, leave them out and the server's
defaults apply.
Missing config fields
When the vault type requires fields that --config does not supply,
vault create and vault migrate name each one with its description and print
the command to re-run. This works for a type that is not installed yet:
$ swamp vault create @swamp/1password op --json{
"error": "Invalid config for vault type '@swamp/1password': missing required field 'op_vault' (The 1Password vault to use). Re-run with: swamp vault create @swamp/1password op --config '{\"op_vault\":\"<op_vault>\"}'"
}A field of the wrong type and an unknown key are explained the same way:
{
"error": "Invalid config for vault type '@swamp/1password':\n - field 'op_vault' expects string, got number\n - unknown field 'bogus'; accepted fields: op_vault, op_account\nRe-run with a corrected --config."
}In an interactive terminal, the command prompts for each missing required string field instead of failing:
$ swamp vault create @swamp/1password op
@swamp/1password needs these config fields. Press Enter to leave one unset.
op_vault (The 1Password vault to use): Privatevault·create Created vault: "op" ("1Password")To fail instead of prompting, pass --json. vault migrate also fails without
prompting under --yes, and both commands do under --server.
swamp vault put <vault_name> <key> [value]
Store a secret in a vault.
| Option | Description |
|---|---|
-y, --yes |
Skip confirmation when overwriting (--force / -f also accepted) |
--refresh-from |
Command to run to refresh the secret value when the TTL expires |
--refresh-ttl |
How long before the secret is considered stale (e.g. 50m, 1h, 30s) |
--clear-refresh |
Remove the refresh hook from this secret |
--label |
Attach a metadata label to the secret (key=value, repeatable). Stored as annotation labels — same as vault annotate --label |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault put my-secrets api-key
Enter value for api-key: ********17:00:04 INF vault·put Stored secret "api-key" in vault "my-secrets"The third-argument (<key> <value>) and inline (KEY=VALUE) forms expose the
secret in shell history, process tables, and system logs. Prefer the interactive
prompt or piped stdin for real credentials.
See Refresh Hooks for details on --refresh-from,
--refresh-ttl, and --clear-refresh. See Annotations for
details on --label.
swamp vault read-secret <vault_name> <key>
Read a secret value from a vault.
| Option | Description |
|---|---|
-y, --yes |
Skip confirmation prompt (--force / -f also accepted) |
--json |
Output structured JSON |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
In interactive (log) mode, prompts for confirmation before revealing the secret
unless --yes is set. In --json mode, outputs the value directly without
prompting.
$ swamp vault read-secret my-secrets api-key --yes19:53:23 INF vault·read-secret "sk-test-12345"$ swamp vault read-secret my-secrets api-key --json{
"vaultName": "my-secrets",
"secretKey": "api-key",
"vaultType": "local_encryption",
"value": "sk-test-12345"
}swamp vault list-keys <vault_name>
List all secret key names in a vault. Values are not shown.
| Option | Description |
|---|---|
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault list-keys my-secrets17:00:29 INF vault·list-keys Vault "my-secrets" ("local_encryption"): 2 key(s)
17:00:29 INF vault·list-keys - "api-key"
17:00:29 INF vault·list-keys - "db-password"swamp vault annotate <vault_name> <key>
Annotate a vault secret with metadata. Attaches provenance metadata (URL, notes,
labels) to an existing secret. Uses merge semantics: only the fields you specify
are updated, existing fields are preserved. Use --clear to remove all
annotations.
| Option | Description |
|---|---|
--url |
URL associated with this secret |
--notes |
Free-text notes about this secret |
--label |
Key=value label (repeatable) |
--remove-label |
Remove a label by key (repeatable) |
--clear |
Remove all annotations from this secret |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault annotate my-secrets api-key \
--url https://console.aws.com/iam \
--notes "Production API key" \
--label env=prod --label team=infraINF vault·annotate Annotated "api-key" in vault "my-secrets" (fields: "url, notes, labels")$ swamp vault annotate my-secrets api-key --clearINF vault·annotate Cleared annotation for "api-key" in vault "my-secrets"swamp vault inspect <vault_name> <key>
Show metadata for a vault secret. Displays size, annotations, refresh hook configuration, and provider capability flags.
| Option | Description |
|---|---|
--json |
Output structured JSON |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault inspect my-secrets api-keyINF vault·inspect Metadata for "api-key" in vault "my-secrets":
INF vault·inspect size: 42 chars (42 bytes)
INF vault·inspect url: "https://console.aws.com/iam"
INF vault·inspect notes: "Production API key"
INF vault·inspect label: "env"="prod"
INF vault·inspect label: "team"="infra"
INF vault·inspect updated: "2026-05-22T23:53:23.906Z"
INF vault·inspect refresh-hook: (none)$ swamp vault inspect my-secrets api-key --json{
"vaultName": "my-secrets",
"secretKey": "api-key",
"vaultType": "local_encryption",
"sizeBytes": 42,
"sizeChars": 42,
"valueType": "string",
"supportsAnnotations": true,
"hasAnnotation": true,
"annotation": {
"updatedAt": "2026-05-22T23:53:23.906Z",
"url": "https://console.aws.com/iam",
"notes": "Production API key",
"labels": {
"env": "prod",
"team": "infra"
}
},
"supportsRefreshHooks": true,
"hasRefreshHook": false,
"refreshHook": null
}swamp vault delete <vault_name> <key>
Delete a secret from a vault. Removes the secret and any associated metadata (annotations, refresh hooks).
Arguments
| Argument | Required | Description |
|---|---|---|
vault_name |
Yes | Name of the vault |
key |
Yes | Secret key to delete |
Flags
| Option | Description |
|---|---|
--repo-dir |
Repository directory (default .) |
-y, --yes |
Skip confirmation prompt and ignore missing keys (--force / -f also accepted) |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
Use --yes to skip the confirmation prompt and to treat non-existent keys as a
no-op. When using --server, the confirmation prompt is not available — use
--yes to suppress the error on missing keys.
$ swamp vault delete my-secrets api-keyDelete secret "api-key" from vault "my-secrets"? [y/N] y
INF vault·delete Deleted secret "api-key" from vault "my-secrets"$ swamp vault delete my-secrets api-key --yesINF vault·delete Deleted secret "api-key" from vault "my-secrets"swamp vault migrate <vault_name>
Migrate a vault to a different backend type. Copies all secrets from the current backend to a new one, then updates the vault configuration. The vault name stays the same, so all existing vault references continue to work.
| Option | Description |
|---|---|
--to-type |
Target vault type (required; always required with --server) |
--config |
Provider-specific configuration as JSON. Missing required fields are named, or prompted for in a terminal — see Missing config fields |
--dry-run |
Preview migration without making changes (not available with --server) |
-y, --yes |
Skip confirmation prompt (--force / -f also accepted) |
--repo-dir |
Repository directory (default .) |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL). Requires --to-type; --dry-run is not available. |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN). |
A local_encryption target migrated over --server gets the server's default
key source; --config cannot set ssh_key_path,
auto_generate, key_file or base_dir to anything else. Run the migration
locally on the serve host to choose them.
$ swamp vault migrate my-secrets --to-type @swamp/aws-sm \
--config '{"region":"us-east-1"}' --dry-run14:23:09 INF vault·migrate Vault ""my-secrets"" ("local_encryption") has 2 secret(s).
14:23:09 INF vault·migrate Target: "AWS Secrets Manager" ("@swamp/aws-sm")
14:23:09 INF vault·migrate Dry run — no changes made.$ swamp vault migrate my-secrets --to-type @swamp/aws-sm \
--config '{"region":"us-east-1"}' --dry-run --json{
"dryRun": true,
"vaultName": "my-secrets",
"currentType": "local_encryption",
"currentTypeName": "Local Encryption",
"targetType": "@swamp/aws-sm",
"targetTypeName": "AWS Secrets Manager",
"secretCount": 2
}swamp vault search [query]
Search for vaults in the repository. Opens an interactive picker when run in a
terminal, or returns JSON with --json.
| Option | Description |
|---|---|
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault search --json{
"query": "",
"results": [
{
"id": "3870058a-0008-48d3-84c1-031405ce9ebf",
"name": "infra",
"type": "local_encryption",
"createdAt": "2026-04-07T17:00:36.582Z"
},
{
"id": "0dd4c691-0b7b-4fae-9741-31e96a4efc69",
"name": "my-secrets",
"type": "local_encryption",
"createdAt": "2026-04-07T16:59:59.039Z"
}
]
}swamp vault get <vault_name_or_id>
Show details of a vault configuration (type, config, storage path). Does not
reveal secret values — use
read-secret to retrieve a secret.
| Option | Description |
|---|---|
--type |
Vault type (narrows search) |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault get my-secrets{
"id": "0dd4c691-0b7b-4fae-9741-31e96a4efc69",
"name": "my-secrets",
"type": "local_encryption",
"config": {
"auto_generate": true,
"base_dir": "/home/user/my-repo"
},
"createdAt": "2026-04-07T16:59:59.039Z",
"storagePath": ".swamp/vault/local_encryption/0dd4c691-0b7b-4fae-9741-31e96a4efc69.yaml"
}swamp vault describe <vault_name_or_id>
Show detailed vault information including configuration.
| Option | Description |
|---|---|
--type |
Vault type (narrows search) |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
swamp vault edit [vault_name_or_id]
Open a vault configuration file in an editor, or replace it with YAML piped on
stdin. Opens an interactive picker when called without arguments and without
--server.
| Option | Description |
|---|---|
--type |
Vault type (narrows search) |
--repo-dir |
Repository directory (default .) |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL). Takes the new YAML on stdin only, and needs the vault name. |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ cat my-vault.yaml | swamp vault edit my-vaultA stdin update keeps the vault's id and createdAt, and cannot change its
type. Under
managedConfig, the
edit applies to the vault config in the datastore config tier.
With --server, stdin is the only edit mode, because the editor cannot run on
the server. Running it with nothing piped fails:
'swamp vault edit' with --server needs the new YAML piped on stdin, because the editor cannot run on the server. For example: cat edited.yaml | swamp vault edit <name> --server <url>Over --server, a local_encryption vault must keep its stored
key-source fields. A vault whose stored YAML no longer
parses can be repaired remotely only with the server's defaults for them.
Renaming a vault
Changing name renames the vault, whether in the editor, on stdin, or over
--server. For a local_encryption vault, the stored secrets and the
auto-generated key move to .swamp/secrets/local_encryption/{new-name}/:
Renamed vault "old-secrets" to "my-secrets"; its stored secrets moved with it- The rename is refused when secrets are already stored under the new name:
Secrets are already stored under vault name 'my-secrets'. Remove them before renaming vault 'old-secrets' to 'my-secrets'. - In the editor, Swamp sees the rename only when the editor waits for the file to close: terminal editors (vim, nano, emacs, helix and others), VS Code, Zed and Sublime Text. A refused rename in the editor is changed back, and your other edits are kept.
- Repairing a vault whose previous YAML did not parse never moves secrets.
- Definitions and workflows still reference the vault by name. Update every
vault.get('old-secrets', ...)to the new name, or those lookups fail.
swamp vault type search [query]
Search for available vault types (built-in and from extensions).
| Option | Description |
|---|---|
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault type search --json{
"query": "",
"results": [
{
"type": "local_encryption",
"name": "Local Encryption",
"description": "Store encrypted secrets in local files using AES-GCM encryption. Uses SSH private key or auto-generated key for encryption."
}
]
}swamp vault audit-trail
View the vault audit trail. Write operations (put, delete, annotate) are
always recorded; read operations (get) are recorded when the vault has
auditReads: true.
| Option | Description |
|---|---|
--vault |
Filter by vault name |
--key |
Filter by secret key |
--action |
Filter by action: get, put, delete, annotate |
--since |
Start date, e.g. 2026-07-01 or ISO 8601 (default: 7 days ago) |
--until |
End date, e.g. 2026-07-01 or ISO 8601 (default: now) |
--limit |
Maximum entries to return (default: 100) |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp vault audit-trailINF vault·audit-trail 3 vault operation(s):
INF vault·audit-trail "2026-07-13T20:53:58.112Z" put "my-secrets" ("local_encryption")/"api-key" by "cli:vault-put-secret"
INF vault·audit-trail "2026-07-13T20:54:21.586Z" get "my-secrets" ("local_encryption")/"api-key" by "cli:vault-read-secret"
INF vault·audit-trail "2026-07-13T20:55:31.276Z" get "my-secrets" ("local_encryption")/"api-key" by "expression:vault-resolve"$ swamp vault audit-trail --vault my-secrets --action getINF vault·audit-trail 2 vault operation(s):
INF vault·audit-trail "2026-07-13T20:54:21.586Z" get "my-secrets" ("local_encryption")/"api-key" by "cli:vault-read-secret"
INF vault·audit-trail "2026-07-13T20:55:31.276Z" get "my-secrets" ("local_encryption")/"api-key" by "expression:vault-resolve"$ swamp vault audit-trail --since 2026-07-14INF vault·audit-trail No vault audit entries found.Vault Audit Trail
The vault audit trail records operations against vault secrets to
.swamp/audit/vault-audit-YYYY-MM-DD.jsonl. Write operations (put, delete,
annotate) are always recorded when an audit repository exists. Read operations
(get) are recorded only for vaults with auditReads: true.
Old audit files using the vault-reads- prefix are still read for backwards
compatibility.
Each line is a JSON object with the following fields:
| Field | Type | Description |
|---|---|---|
timestamp |
string | ISO 8601 timestamp of the operation (Z suffix) |
action |
string | Operation type: get, put, delete, or annotate |
vaultName |
string | Name of the vault |
vaultType |
string | Vault provider type (e.g. local_encryption) |
secretKey |
string | Key that was operated on |
callerContext |
string | What triggered the operation (see values below) |
callerContext values
| Value | Trigger |
|---|---|
cli:vault-read-secret |
swamp vault read-secret |
expression:vault-resolve |
vault.get() CEL expression during model or workflow execution |
The set of callerContext values is extensible — new integration points and
write operations may introduce additional values.
Example entry
{
"timestamp": "2026-07-13T20:54:21.586Z",
"action": "get",
"vaultName": "my-secrets",
"vaultType": "local_encryption",
"secretKey": "api-key",
"callerContext": "cli:vault-read-secret"
}Complete Example
Create a vault, store secrets, and reference them in a model definition:
swamp vault create local_encryption prod-secrets
echo "$API_KEY" | swamp vault put prod-secrets api-key
echo "$DB_PASSWORD" | swamp vault put prod-secrets db-passwordReference the secrets in a model definition:
type: command/shell
typeVersion: 2026.02.09.1
id: 53274ce2-c390-412b-93ac-48b205a13f4e
name: api-caller
version: 1
globalArguments:
api_key: "${{ vault.get('prod-secrets', 'api-key') }}"
methods:
execute:
arguments:
run: "curl -H \"Authorization: Bearer ${{ vault.get('prod-secrets', 'api-key') }}\" https://api.example.com"
env:
DB_PASSWORD: "${{ vault.get('prod-secrets', 'db-password') }}"At execution time, vault.get() expressions resolve to the stored secret
values. Shell commands receive secrets as environment variables — the raw values
never appear in the command string.
Remote execution
When a workflow step runs on a remote worker, vault secrets are resolved on the orchestrator before the step is dispatched. The resolved values are transmitted to the worker over the encrypted data plane for the duration of the dispatch. Workers never receive vault credentials or connection details — they see only the resolved plaintext for secrets referenced by the current step. See the Security reference for the full isolation model.