Skip to content

feat(cli): add volume and secret management subcommands - #1989

Closed
lemonzoey wants to merge 1 commit into
e2b-dev:mainfrom
lemonzoey:zhouyi/feat-cli-volume-secret
Closed

lemonzoey wants to merge 1 commit into
e2b-dev:mainfrom
lemonzoey:zhouyi/feat-cli-volume-secret

Conversation

@lemonzoey

@lemonzoey lemonzoey commented Oct 15, 2026 •

Copy link
Copy Markdown

Summary

The JS and Python SDKs ship full Volume and Secret management (create/list/info/delete, plus secret update), backed by the /volumes and /secrets endpoints (openapi.yml — Volume: #1126, Secret: #1728, both merged). The CLI exposed neither: there was no e2b volume or e2b secret command, so a user working from the terminal couldn't manage volumes or secrets without dropping to the SDK or the raw API. sandbox and template are already first-class CLI resources; volumes and secrets were the gap. Closes #1988.

What's added

Two top-level resource subcommands mirroring the SDK surface, following the existing e2b sandbox / e2b sandbox snapshot command style (commander chain, ensureAPIKey, renderTable, --format json|pretty):

e2b volume create <name>
e2b volume list [--format json]
e2b volume info <volumeID>
e2b volume delete <volumeID>   (alias: rm)

e2b secret create <name> --value <value> [--metadata key=value]
e2b secret list [--limit N] [--format json]
e2b secret info <secret>
e2b secret update <secret> --value <value> [--metadata key=value]
e2b secret delete <secret>    (alias: rm)

Secret values are write-only (never returned by the API), so they are never printed — only ID/name/version/metadata/timestamps are.

Usage example

# Volumes
e2b volume create my-data
# Created volume my-data (ID: vol_...)
e2b volume list
# VOLUME ID    NAME
# vol_...      my-data
e2b volume delete vol_...
# Volume vol_... has been deleted

# Secrets
e2b secret create API_KEY --value "sk-..." --metadata env=prod
# Created secret API_KEY (ID: secret_..., version 1)
e2b secret list --format json
# [{"secretId":"secret_...","name":"API_KEY","version":1,...}]
e2b secret update API_KEY --value "sk-new..."
# Updated secret API_KEY (ID: secret_..., version 2)

Scope / non-goals

  • Volume metadata is out of scope — that's Feature Request: Add metadata parameter to volume creation (similar to sandbox create) #1643, blocked on the NewVolume schema gaining a metadata field in the belt repo. The four CRUD operations here need no spec change (endpoints already exist, SDK already implements them).
  • Volume mount is a sandbox-create concern, not a standalone volume operation, so it's not part of e2b volume (consistent with how the SDK models it).
  • No new API endpoints or spec changes — purely the CLI surface over the existing SDK + openapi.

Testing

  • tests/commands/secret/list.test.ts: unit tests for the secret list row formatter (metadata stringification, timestamp formatting, immutability).
  • tests/commands/volume_secret_commands.test.ts: asserts both command groups register their operations (volume: create/list/info/delete; secret: create/list/info/update/delete), aliases (ls, rm), and that secret create requires --value.
  • Full CLI suite: 141 passed, 8 skipped (skips need E2B_API_KEY); format / lint / typecheck clean.

🤖 Generated with kscc


Devin Review

The SDKs ship full Volume and Secret management (create/list/info/delete,
plus secret update), backed by the `/volumes` and `/secrets` endpoints,
but the CLI exposed neither — a user working from the terminal could not
manage volumes or secrets without dropping to the SDK or the raw API.
sandbox and template are already first-class CLI resources; volumes and
secrets were the gap.

Add two top-level resource subcommands mirroring the SDK surface, following
the existing `e2b sandbox` / `e2b sandbox snapshot` style (commander chain,
`ensureAPIKey`, `renderTable`, `--format json|pretty`):

  e2b volume create <name>
  e2b volume list [--format json]
  e2b volume info <volumeID>
  e2b volume delete <volumeID>   (alias: rm)

  e2b secret create <name> --value <value> [--metadata key=value]
  e2b secret list [--limit N] [--format json]
  e2b secret info <secret>
  e2b secret update <secret> --value <value> [--metadata key=value]
  e2b secret delete <secret>    (alias: rm)

Secret values are write-only (never returned by the API), so they are
never printed; only ID/name/version/metadata/timestamps are. Volume
metadata is out of scope (that's e2b-dev#1643, blocked on the NewVolume schema
gaining a metadata field); the four CRUD operations need no spec change.

Tests: a unit suite for the secret list row formatter plus a command-tree
test asserting both groups register their operations and aliases.

Closes e2b-dev#1988

Co-Authored-By: Kscc <noreply@owtffssent.com>
@changeset-bot

changeset-bot Bot commented Oct 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 35d928c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@e2b/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T06:24:32.807944Z 35d928c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TASTE.md compliance review (CLI-only PR; checked against the rules that apply to code consuming the SDK surface)

Checked: T-7 (ID-or-name arguments), T-11 (named types over ad-hoc shapes), T-12 (acronym casing), T-32 (handling destroy's false instead of an exception), T-34/T-35 (draining the paginator via while (hasNext) nextItems()), T-47 (named default constants), T-52 (no client-side mirroring of backend rules).

1 violation found (T-11), at 2 places — see the inline comments. Everything else matches: secret list drains Secret.list() with the sanctioned hasNext/nextItems() loop, DEFAULT_LIMIT/PAGE_LIMIT are named constants, both delete commands treat destroy() → false as "wasn't found" and don't expect it to throw, and no backend validation is duplicated.

Notes not tied to a changed line (not counted as violations):

  • T-7 asymmetry: secret info/update/delete take <secretID> or name, but volume info/delete take only <volumeID>. That follows the SDK (Volume.getInfo(volumeId) / Volume.destroy(volumeId)) and the /volumes/{volumeID} endpoint, so the PR can't fix it on its own. The fix belongs in the SDK, and the CLI would get name support from there.
  • T-12: the new locals and placeholders are volumeID / <volumeID> / <secretID>. T-12 asks for volumeId, but the existing CLI uses sandboxID throughout, so I left it alone. Matching the surrounding code wins here.
  • Outside TASTE.md: volume info --format json serializes the full VolumeAndToken, so the volume access token is printed. That's the opposite of how this PR treats secret values. Worth deciding whether that's intended.

Written by Devin

Comment on lines +14 to +21
export function formatSecretRow(secret: {
secretId: string
name: string
version: number
metadata: Record<string, string>
createdAt: Date
updatedAt: Date
}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-11 — When a concept already has a named type, use it instead of re-spelling its shape. The SDK exports SecretInfo from e2b. If this inline copy drifts from it (e.g. a field gets added or made optional), it stops matching without warning.

Suggested change
export function formatSecretRow(secret: {
secretId: string
name: string
version: number
metadata: Record<string, string>
createdAt: Date
updatedAt: Date
}) {
export function formatSecretRow(secret: SecretInfo) {

(and import type { SecretInfo } from 'e2b' next to the Secret import)

pageLimit = PAGE_LIMIT
}

const secrets: Awaited<ReturnType<typeof Secret.getInfo>>[] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-11 — Same here. Awaited<ReturnType<typeof Secret.getInfo>> rebuilds a type that already has a name, and SecretPaginator.nextItems() returns SecretInfo[]. Use the named type:

Suggested change
const secrets: Awaited<ReturnType<typeof Secret.getInfo>>[] = []
const secrets: SecretInfo[] = []

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

) => {
try {
const apiKey = ensureAPIKey()
const metadata = parseMetadata(options.metadata?.join(','))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Secret metadata values lose delimiters

When metadata values contain commas or equals signs, parseMetadata truncates them before Secret.create or Secret.update. The stored metadata differs from the supplied value.

Learn more

Secret metadata is stored as a string map; the API does not restrict values to strings without commas or equals signs SecretMetadata. Both create and update join repeated options with commas and pass them through parseMetadata. That parser splits first on commas and then on every equals sign, silently dropping the rest. The same failure affects the update path at the second parseMetadata call.

Example: e2b secret create KEY --value x --metadata 'url=https://example.com?a=1,b=2' stores url=https://example.com?a instead of the supplied URL.

Recommended fix: Parse each repeated --metadata option independently, split only at its first =, and retain the entire remainder as the value. Reject malformed entries rather than silently discarding them.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +103 to +105
while (paginator.hasNext && (!limit || secrets.length < limit)) {
const batch = await paginator.nextItems()
secrets.push(...batch)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Negative limit hides existing secrets

With --limit -1, Secret.list never fetches a page because the loop compares zero items against a negative limit. The command reports no secrets despite existing ones.

Learn more

The --limit parser accepts signed numbers without checking their range. A negative limit makes secrets.length < limit false before the first request, so no secrets are returned even when the project has secrets. A malformed limit parsed as NaN has the same effect.

Example: With one stored secret, e2b secret list --limit -1 --format json prints [] without sending a list request.

Recommended fix: Validate --limit as a nonnegative integer in its Commander option parser; reject invalid input before creating the paginator.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +22 to +24
if (options.format === 'json') {
console.log(JSON.stringify(info, null, 2))
} else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Unsupported output formats silently succeed

With --format yaml, the new commands print pretty output and exit successfully. Existing list commands reject unsupported formats, so callers can mistake plain text for requested structured output.

Learn more

All new volume and secret commands branch on format === 'json' and treat every other string as pretty. Existing sandbox list and template list reject unknown formats. This matters for scripts that request a specific format and trust the exit status before processing the output.

Example: e2b volume list --format yaml succeeds with a plain-text table rather than rejecting yaml.

Recommended fix: Validate the format in all new create, list, and info/update commands, accepting only json and pretty, and exit nonzero for any other value.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

const info = await Volume.getInfo(volumeID, { apiKey })

if (options.format === 'json') {
console.log(JSON.stringify(info, null, 2))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Volume credential leaked through JSON info

With --format json, Volume.getInfo includes a volume access token in the output. Logs or shared output expose that bearer credential.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +36 to +39
.requiredOption(
'-v, --value <value>',
'secret value (write-only — never returned by the API)'
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Secret values exposed in process arguments

Secret create and update require --value, exposing credentials in shell history and process listings. A write-only API response does not protect the input.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35d928c63b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const info = await Volume.getInfo(volumeID, { apiKey })

if (options.format === 'json') {
console.log(JSON.stringify(info, null, 2))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact the volume bearer token from JSON info output

When e2b volume info <id> --format json is used, Volume.getInfo returns a VolumeAndToken and this line serializes its token; VolumeConnectionConfig installs that value as the Bearer credential for volume-content reads and writes. Pretty output and volume create --format json already omit it, so JSON output that is captured or logged unexpectedly discloses a scoped credential; project the public info fields instead or require an explicit token-export option.

Useful? React with 👍 / 👎.

) => {
try {
const apiKey = ensureAPIKey()
const metadata = parseMetadata(options.metadata?.join(','))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve delimiters inside secret metadata values

When a metadata value contains =—for example --metadata callback='https://host/?a=b'—this passes the joined string to parseMetadata, which destructures pair.split('=') and silently persists only https://host/?a; commas are likewise reinterpreted as new entries. The API schema accepts arbitrary string values, and updateCommand repeats this path, so parse each repeatable option at its first separator rather than recombining it.

Useful? React with 👍 / 👎.

@@ -0,0 +1,5 @@
---
'@e2b/cli': patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish new CLI commands with a minor bump

These changes add two public top-level command groups, but this entry tells Changesets to release @e2b/cli as 2.22.1. Comparable command additions such as sandbox snapshot and sandbox fork were minor releases, and SemVer reserves patch bumps for fixes; mark this minor so consumers and release automation identify the new CLI surface correctly.

AGENTS.md reference: AGENTS.md:L6-L6

Useful? React with 👍 / 👎.

Comment on lines +22 to +24
if (options.format === 'json') {
console.log(JSON.stringify(info, null, 2))
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unsupported output formats

When any new action that accepts --format is given yaml or a typo such as jsno, this binary branch and its counterparts in the volume and secret actions treat it as pretty output and exit successfully. Existing sandbox list, sandbox info, and snapshot-list commands reject values other than json or pretty, so automation can receive human-formatted output under an unsupported format; validate the option and fail before emitting output.

Useful? React with 👍 / 👎.

@mishushakov

Copy link
Copy Markdown
Member

Hey, thanks for trying to contribute here. We will handle this one internally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI: expose volume and secret management subcommands

2 participants