Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions .claude/skills/swamp-vault/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ Correct flow: `swamp vault create <type> <name> --json` → edit config if neede

## Quick Reference

| Task | Command |
| ----------------- | -------------------------------------------------- |
| List vault types | `swamp vault type search --json` |
| Create a vault | `swamp vault create <type> <name> --json` |
| Search vaults | `swamp vault search [query] --json` |
| Get vault details | `swamp vault get <name_or_id> --json` |
| Edit vault config | `swamp vault edit <name_or_id>` |
| Store a secret | `swamp vault put <vault> KEY=VALUE --json` |
| Store from stdin | `echo "val" \| swamp vault put <vault> KEY --json` |
| Store interactive | `swamp vault put <vault> KEY` (prompts for value) |
| Get a secret | `swamp vault get <vault> <key> --json` |
| List secret keys | `swamp vault list-keys <vault> --json` |
| Migrate backend | `swamp vault migrate <vault> --to-type <type>` |
| Task | Command |
| ----------------- | ------------------------------------------------------ |
| List vault types | `swamp vault type search --json` |
| Create a vault | `swamp vault create <type> <name> --json` |
| Search vaults | `swamp vault search [query] --json` |
| Get vault details | `swamp vault get <name_or_id> --json` |
| Edit vault config | `swamp vault edit <name_or_id>` |
| Store a secret | `swamp vault put <vault> KEY=VALUE --json` |
| Store from stdin | `echo "val" \| swamp vault put <vault> KEY --json` |
| Store interactive | `swamp vault put <vault> KEY` (prompts for value) |
| Read a secret | `swamp vault read-secret <vault> <key> --force --json` |
| List secret keys | `swamp vault list-keys <vault> --json` |
| Migrate backend | `swamp vault migrate <vault> --to-type <type>` |

## Repository Structure

Expand Down Expand Up @@ -158,26 +158,31 @@ agent context or chat history.
}
```

## Get a Secret
## Read a Secret

Retrieve a specific secret value from a vault.

```bash
swamp vault get dev-secrets API_KEY --json
# With --force to skip confirmation prompt
swamp vault read-secret dev-secrets API_KEY --force --json

# Interactive mode prompts before revealing
swamp vault read-secret dev-secrets API_KEY
```

**Output shape:**
**Output shape (--json):**

```json
{
"vault": "dev-secrets",
"key": "API_KEY",
"vaultName": "dev-secrets",
"secretKey": "API_KEY",
"vaultType": "local_encryption",
"value": "sk-1234567890"
}
```

**Note:** Use with caution. Secret values are sensitive and should not be logged
or displayed unnecessarily.
In log mode without `--force`, prompts for confirmation before displaying the
value. In `--json` mode, outputs directly without prompting.

## List Secret Keys

Expand Down Expand Up @@ -223,7 +228,7 @@ at model creation time and prevents rotation or in-workflow refresh:

```bash
# WRONG — frozen at creation time
TOKEN=$(swamp vault get my-vault AUTH_TOKEN)
TOKEN=$(swamp vault read-secret my-vault AUTH_TOKEN --force)
swamp model create ... --global-arg "token=$TOKEN"

# RIGHT — resolved fresh per-step
Expand Down
32 changes: 32 additions & 0 deletions design/vaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,38 @@ The expression syntax is:
- `key` - The secret identifier within that vault
- `value` - The value to store (for put operations)

## CLI Secret Retrieval

The `swamp vault read-secret` command reads a secret value from a vault via CLI:

```
swamp vault read-secret <vault_name> <key> [--force] [--json]
```

This calls `VaultService.get()` — the same method used by `vault.get()` CEL
expressions — through a dedicated CLI surface. No new `VaultProvider` interface
methods are required.

### Safety Model

- **Log mode**: Prompts for confirmation before revealing the secret. Use
`--force` (`-f`) to skip the prompt.
- **JSON mode**: Outputs directly without prompting (designed for agent/script
consumption).
- **Audit**: Every CLI read emits a `VaultSecretRead` domain event through the
event bus, recording the vault name, type, and secret key accessed.

### JSON Output

```json
{
"vaultName": "my-vault",
"secretKey": "API_KEY",
"vaultType": "local_encryption",
"value": "sk-test-..."
}
```

## Sensitive Field Marking (Implemented)

Model schemas mark fields as sensitive using Zod's `.meta()` method. When a
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { vaultEditCommand } from "./vault_edit.ts";
import { vaultPutCommand } from "./vault_put.ts";
import { vaultListKeysCommand } from "./vault_list_keys.ts";
import { vaultMigrateCommand } from "./vault_migrate.ts";
import { vaultReadSecretCommand } from "./vault_read_secret.ts";
import { unknownCommandErrorHandler } from "../unknown_command_handler.ts";

/**
Expand Down Expand Up @@ -69,6 +70,7 @@ export const vaultCommand = new Command()
.command("edit", vaultEditCommand)
.command("put", vaultPutCommand)
.command("migrate", vaultMigrateCommand)
.command("read-secret", vaultReadSecretCommand)
.command("list-keys", vaultListKeysCommand)
.command(
"list",
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/vault_get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const vaultGetCommand = new Command()
throw new UserError(
`Unexpected argument: ${extra}\n\n` +
"Usage: swamp vault get <vault_name_or_id>\n\n" +
"To retrieve a secret value, use: swamp vault list-keys <vault_name>",
"To retrieve a secret value, use: swamp vault read-secret <vault_name> <key>",
);
}

Expand Down
107 changes: 107 additions & 0 deletions src/cli/commands/vault_read_secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Swamp, an Automation Framework
// Copyright (C) 2026 System Initiative, Inc.
//
// This file is part of Swamp.
//
// Swamp is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation, with the Swamp
// Extension and Definition Exception (found in the "COPYING-EXCEPTION"
// file).
//
// Swamp is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Swamp. If not, see <https://www.gnu.org/licenses/>.

import { Command } from "@cliffy/command";
import {
consumeStream,
createLibSwampContext,
createVaultReadSecretDeps,
vaultReadSecret,
} from "../../libswamp/mod.ts";
import { createVaultReadSecretRenderer } from "../../presentation/renderers/vault_read_secret.ts";
import {
createContext,
type GlobalOptions,
resolveRepoDir,
} from "../context.ts";
import { requireInitializedRepo } from "../repo_context.ts";

// deno-lint-ignore no-explicit-any
type AnyOptions = any;

async function promptConfirmation(message: string): Promise<boolean> {
const encoder = new TextEncoder();
const decoder = new TextDecoder();

await Deno.stdout.write(encoder.encode(`${message} [y/N] `));

const buf = new Uint8Array(1024);
const n = await Deno.stdin.read(buf);
if (n === null) return false;

const response = decoder.decode(buf.subarray(0, n)).trim().toLowerCase();
return response === "y" || response === "yes";
}

export const vaultReadSecretCommand = new Command()
.name("read-secret")
.description(
`Read a secret value from a vault.

In interactive (log) mode, prompts for confirmation before revealing the secret
unless --force is set. In --json mode, outputs the value directly.`,
)
.example("Read a secret", "swamp vault read-secret my-vault API_KEY --force")
.example(
"Read a secret (JSON output)",
"swamp vault read-secret my-vault API_KEY --json",
)
.arguments("<vault_name:string> <key:string>")
.option(
"--repo-dir <dir:string>",
"Repository directory (env: SWAMP_REPO_DIR)",
)
.option("-f, --force", "Skip confirmation prompt")
.action(async function (
options: AnyOptions,
vaultName: string,
key: string,
) {
const cliCtx = createContext(options as GlobalOptions, [
"vault",
"read-secret",
]);
cliCtx.logger.debug`Reading secret from vault: ${vaultName}`;

const { repoDir, repoContext } = await requireInitializedRepo({
repoDir: resolveRepoDir(options.repoDir),
outputMode: cliCtx.outputMode,
});

if (cliCtx.outputMode === "log" && !options.force) {
const confirmed = await promptConfirmation(
`This will reveal the secret '${key}' from vault '${vaultName}'. Continue?`,
);
if (!confirmed) {
cliCtx.logger.info`Cancelled.`;
return;
}
}

const ctx = createLibSwampContext({ logger: cliCtx.logger });
const deps = createVaultReadSecretDeps(repoDir, repoContext.eventBus);

const renderer = createVaultReadSecretRenderer(cliCtx.outputMode);
await consumeStream(
vaultReadSecret(ctx, deps, { vaultName, secretKey: key }),
renderer.handlers(),
);

cliCtx.logger.debug("Vault read-secret command completed");
});
33 changes: 32 additions & 1 deletion src/domain/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ export type RepositoryEvent =
| VaultCreated
| VaultUpdated
| VaultDeleted
| VaultSecretUpdated;
| VaultSecretUpdated
| VaultSecretRead;

/**
* Event type discriminator values.
Expand Down Expand Up @@ -493,6 +494,17 @@ export function createVaultDeleted(
};
}

/**
* Emitted when a secret is read from a vault via CLI.
*/
export interface VaultSecretRead extends DomainEvent {
readonly type: "VaultSecretRead";
readonly vaultId: string;
readonly vaultType: string;
readonly vaultName: string;
readonly secretKey: string;
}

/**
* Creates a VaultSecretUpdated event.
*/
Expand All @@ -511,3 +523,22 @@ export function createVaultSecretUpdated(
timestamp: new Date(),
};
}

/**
* Creates a VaultSecretRead event.
*/
export function createVaultSecretRead(
vaultId: string,
vaultType: string,
vaultName: string,
secretKey: string,
): VaultSecretRead {
return {
type: "VaultSecretRead",
vaultId,
vaultType,
vaultName,
secretKey,
timestamp: new Date(),
};
}
10 changes: 10 additions & 0 deletions src/libswamp/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,16 @@ export {
type VaultListKeysInput,
} from "./vaults/list_keys.ts";

// Vault read-secret operations
export {
createVaultReadSecretDeps,
vaultReadSecret,
type VaultReadSecretData,
type VaultReadSecretDeps,
type VaultReadSecretEvent,
type VaultReadSecretInput,
} from "./vaults/read_secret.ts";

// Vault migrate operations
export {
createVaultMigrateDeps,
Expand Down
Loading
Loading