Skip to main content

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.enc

The 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:

Listing Keys

$ swamp vault list-keys my-secrets
17: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=infra
INF vault·annotate Annotated "api-key" in vault "my-secrets" (fields: "url, notes, labels")

Inspecting Metadata

$ swamp vault inspect my-secrets api-key
INF 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-password
INF 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 --yes
INF 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_TOKEN
INF 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: never

After 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:

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>.outputs or model.<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

  1. During expression evaluation, each vault.get() call, and each sensitive field value inside an expression's result, is replaced with an internal sentinel token.
  2. Before shell execution, each sentinel is replaced with a shell variable reference (${__SWAMP_VAULT_N}), where N is a sequential index.
  3. The raw secret values are passed as environment variables to the process.
  4. 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-12345

A 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 instead

In 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 }}
        EOF

The 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}
EOF

No 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 instead

In 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_approval prompt 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:

  1. Field-level vaultName from schema metadata (set by extension authors via .meta({ sensitive: true, vaultName: "..." }) in TypeScript)
  2. Spec-level vaultName from the ResourceOutputSpec — set by extension authors in TypeScript, or overridden per-definition via the resources.<specName>.vaultName field in the definition YAML
  3. Repo-level defaultVault from .swamp.yaml — applies to model execution, swamp serve OAuth token storage, and device authorization token storage
  4. 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

  1. Lists all secret keys in the source vault
  2. Copies each secret value from the current backend to a new provider instance
  3. Updates the vault configuration file to point to the new backend type (new config is written before the old one is removed)
  4. 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-secrets
17: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): Private
vault·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 --yes
19: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-secrets
17: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=infra
INF vault·annotate Annotated "api-key" in vault "my-secrets" (fields: "url, notes, labels")
$ swamp vault annotate my-secrets api-key --clear
INF 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-key
INF 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-key
Delete 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 --yes
INF 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-run
14: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-vault

A 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-trail
INF 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 get
INF 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-14
INF 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-password

Reference 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.