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

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)

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"}'

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:

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 --force (-f) 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.

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 Annotations

$ swamp vault inspect my-secrets api-key
INF vault·inspect Annotation for "api-key" in vault "my-secrets":
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"

JSON output:

$ swamp vault inspect my-secrets api-key --json
{
  "vaultName": "my-secrets",
  "secretKey": "api-key",
  "vaultType": "local_encryption",
  "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"
    }
  }
}

When no annotation exists:

$ swamp vault inspect my-secrets api-key
INF vault·inspect No annotation found for "api-key" in vault "my-secrets"

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 --force
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   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   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 hasRefreshHook and refreshHook fields:

$ swamp vault inspect my-vault GCP_TOKEN --json
{
  "vaultName": "my-vault",
  "secretKey": "GCP_TOKEN",
  "vaultType": "local_encryption",
  "hasAnnotation": false,
  "annotation": null,
  "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.


Environment Variable Mounting

When a shell command contains vault.get() expressions, swamp resolves the secrets and passes them to the executing process as environment variables. The actual secret values never appear in the command string.

Resolution Process

  1. During expression evaluation, each vault.get() call 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:

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.


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
  2. Spec-level vaultName from the resource output specification
  3. First available vault from the vault service

If sensitive fields exist but no vault is configured, an error is thrown.

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.


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

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.

Option Description
--config Provider configuration as JSON
--repo-dir Repository directory (default .)
$ 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")

swamp vault put <vault_name> <key> [value]

Store a secret in a vault.

Option Description
-f Skip confirmation when overwriting
--force Same as -f
--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
$ 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.

swamp vault read-secret <vault_name> <key>

Read a secret value from a vault.

Option Description
-f Skip confirmation prompt
--force Same as -f
--json Output structured JSON

In interactive (log) mode, prompts for confirmation before revealing the secret unless --force is set. In --json mode, outputs the value directly without prompting.

$ swamp vault read-secret my-secrets api-key --force
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.

$ 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
$ 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 annotations for a vault secret. Displays the metadata (URL, notes, labels) attached to a secret via swamp vault annotate.

Option Description
--json Output structured JSON
$ swamp vault inspect my-secrets api-key
INF vault·inspect Annotation for "api-key" in vault "my-secrets":
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"
$ swamp vault inspect my-secrets api-key --json
{
  "vaultName": "my-secrets",
  "secretKey": "api-key",
  "vaultType": "local_encryption",
  "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"
    }
  }
}

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)
--config Provider-specific configuration as JSON
--dry-run Preview migration without making changes
-f Skip confirmation prompt
--force Same as -f
--repo-dir Repository directory (default .)
$ 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.

$ 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)
$ 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)

swamp vault edit [vault_name_or_id]

Open a vault configuration file in an editor. Opens an interactive picker when called without arguments.

Option Description
--type Vault type (narrows search)

swamp vault type search [query]

Search for available vault types (built-in and from extensions).

$ 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."
    }
  ]
}

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.