Stacked on #1680.
Auth moved to Hydra OAuth in #1481, which left `ensureAccessToken()`
with no callers and the module-level API client attaching a stale
`Authorization: Bearer <access token>` header to every request. This
drops `ensureAccessToken()`, the `accessToken` export, the
`E2B_ACCESS_TOKEN` arm of the auth error box, and the `apiHeaders`
wiring on `connectionConfig` — the only consumers of that header were
`template list`/`create`, and `/templates` is scoped by the API key
alone, while `auth login` / `auth configure` build their own clients
with Hydra JWTs. `requireApiKey: false` stays on the shared client, but
its justification is now that `e2b auth login` runs before any API key
exists and the client is built at import time.
User-facing effect: combined with #1680, the CLI ignores
`E2B_ACCESS_TOKEN` entirely, so CI setups can drop it and keep only the
API key.
```bash
# Before: both were commonly set in CI
export E2B_ACCESS_TOKEN=sk_e2b_...
export E2B_API_KEY=e2b_...
# Now: the API key alone authorizes everything the CLI calls
export E2B_API_KEY=e2b_...
e2b template list
e2b template create my-template
```
Part of
[SDK-6](https://linear.app/e2b/issue/SDK-6/mark-e2b-access-token-as-deprecated-inside-all-code-references).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exposes `--user`, `--cwd`, and repeatable `--env KEY=VALUE` flags on
`e2b sandbox create` (and the deprecated `spawn` alias) and `e2b sandbox
connect`, forwarding them to the underlying PTY session so the connected
terminal starts as the given user, in the given working directory, and
with the given environment variables. The SDKs already supported these
PTY options — this just wires them through the CLI. The `--env` arg
parser is extracted into a shared `src/utils/env.ts` and reused across
`create`, `connect`, and `exec`. Added unit tests for the parser and CLI
tests covering the new flags; a changeset is included for `@e2b/cli`.
## Usage
```bash
# Start the terminal as root, in /app, with custom env vars
e2b sandbox create base --user root --cwd /app --env FOO=bar --env TOKEN=abc123
# Same flags when attaching to an already-running sandbox
e2b sandbox connect <sandboxID> --user root --cwd /app --env FOO=bar
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes
[SDK-298](https://linear.app/e2b/issue/SDK-298/release-tags-point-at-the-commit-before-the-version-bump).
Replaces #1615, which moved the tags after the fact instead of removing
the reason they were misplaced.
## The bug
Every published release tag pointed at the commit *preceding* its own
version bump:
```console
$ git show '@e2b/python-sdk@2.35.0:packages/python-sdk/pyproject.toml' | head -3
[project]
name = "e2b"
version = "2.34.0" # ← tagged 2.35.0
```
Anything that builds from a git tag rather than a registry got the
previous release: distro packagers, `pip install git+…@tag`, any bisect
over a release regression. `python3Packages.e2b` in nixpkgs shipped
1.5.0 as 1.5.1 from June 2025.
## Root cause: a dependency cycle
`changeset publish` tags whatever commit it publishes from, so the fix
is to commit the version bump first. That was impossible:
```
tag must point at → release commit
release commit must contain → pnpm-lock.yaml
pnpm-lock.yaml contains → integrity hash of a tarball this release uploads
```
`packages/cli` depended on `e2b` by registry range, so `changeset
version` rewrote that range and the lockfile had to be re-resolved
against a tarball that did not exist yet. The lockfile could only be
refreshed *after* publishing, which forced the commit — and therefore
the tags — after it too.
## The fix
`packages/cli`: `"e2b": "^2.36.1"` → `"e2b": "workspace:^"`.
The lockfile now records `link:../js-sdk` and stops changing at release
time, so the release commit is complete before anything is uploaded:
| | before | after |
|---|---|---|
| 1 | `pnpm run version` | `pnpm run version` |
| 2 | publish **+ tag** ← wrong commit | **commit** (local) |
| 3 | refresh `pnpm-lock.yaml` (retry ≤6×) | publish **+ tag** ← right
commit |
| 4 | commit + push | push |
That deletes the lockfile-refresh step and its whole
registry-propagation retry loop (#1589), and `createGithubReleases:
true` keeps doing the tagging and GitHub releases — no custom tagging
code. Keeping the commit local also improves recovery: a publish that
uploads *nothing* leaves the branch untouched with the changesets
intact, so re-dispatching retries cleanly.
### Landing that commit is now mandatory, so the push is resilient
Once the tags point at a local commit, getting it onto the branch stops
being bookkeeping. `changesets/action` pushes each tag as soon as
`changeset publish` reports it (`runPublish` → `git.pushTag`), *before*
it propagates a non-zero exit — so three things changed:
- **The push is gated on the tags themselves** — `git tag --points-at
HEAD` — not on whether the publish step succeeded. The tags are the
thing that has to end up reachable, so they are the right thing to ask.
A partial failure (npm succeeds, then python-sdk's `postPublish` fails
on PyPI) used to skip the push and strand tags on a commit that reached
no branch while `main` kept the old versions.
I first wrote this as `!cancelled() && (success() ||
steps.release.outputs.published == 'true')`, which was wrong in both
directions: `success()` fires in exactly the case that must be skipped
(publish exits 0 having uploaded nothing → pushes a bump with no tags,
cementing a version that can never be published), and `published` is
left unset when the action *throws* after tagging (`core.setOutput` runs
only on a normal return from `runPublish`, but `git.pushTag` happens
inside it) — so it was skipped in the very case it existed for. The tag
gate also covers `@e2b/python-sdk`, which the npm-derived output never
did, since `privatePackages.tag` is on.
- **A partial publish is reported, not swallowed.** It still has to land
— otherwise the pushed tags hang off no branch — but the bump is then on
the branch with the changesets consumed, so re-dispatching will not
retry what failed. The step now names the tags that did land and points
out that `postPublish`'s PyPI upload was skipped (the root script is
`changeset publish && ... postPublish`, so a non-zero npm exit
short-circuits it).
- **A non-fast-forward is reconciled with a merge,** not a rebase (which
would orphan the tags) and not a hard failure. Hard-failing left an
already-published release needing manual git surgery, and a naive
re-dispatch would publish nothing (versions already on the registry),
tag nothing, and report **success** — quietly recreating SDK-298.
- **`git add -A` replaces `commit -am`,** which cannot stage new files.
`changeset version` writes each `CHANGELOG.md` fresh, so no release
commit has ever contained one:
```console
$ git show --stat cf8296cf8 | tail -4
.changeset/lucky-pandas-wave.md | 5 ---
packages/cli/package.json | 4 +-
packages/js-sdk/package.json | 2 +-
pnpm-lock.yaml | 77 +-----------------
```
## The published packages do not change
`pnpm publish` (which `changeset publish` uses in a pnpm workspace)
rewrites the protocol. Verified on the real CLI package with the
workspace SDK at 9.9.9:
```
e2b dependency -> ^9.9.9
PASS: no workspace: in published manifest
```
## Verification
| check | result |
|---|---|
| `pnpm install --frozen-lockfile` on a clean clone | consistent |
| `pnpm run version` touches the lockfile? | **no** — `git diff
pnpm-lock.yaml` empty after bumping sdk 2.36.1→2.36.2, cli→2.16.1 |
| tags land on the release commit | `PASS e2b@2.36.2`, `PASS
@e2b/python-sdk@2.36.0`; tagged trees contain `"version": "2.36.2"` /
`version = "2.36.0"` |
| CLI still bumped when the SDK is | yes, `updateInternalDependencies`
still sees the internal dep |
| CLI typecheck / tests | clean / 102 passed (1 pre-existing failure
needs `E2B_API_KEY` + a built `dist`) |
| CLI bundle | builds, contains the workspace SDK, zero external
`require("e2b")` |
| `pnpm publish` git checks | `changeset publish` passes
`--no-git-checks` for pnpm ≥5 (repo pins 9.15.5); added explicitly to
the RC flows, which publish from a feature branch with an uncommitted
bump |
| `prepack` guard | `npm pack` fails and produces no tarball; `pnpm
pack` passes and rewrites to `^2.36.1` |
| `pnpm publish` lifecycle | runs `prepublishOnly` + `prepack` +
`prepare`, so the RC still builds; `pnpm pack` runs only `prepack` +
`prepare` (0.37 s, no rebuild) |
| `pnpm publish --provenance` | flag accepted (pnpm forwards to the npm
publish it spawns) |
| `pnpm link --global` | links the workspace CLI (2.16.0) and resolves
`workspace:^`; tested against an isolated `PNPM_HOME` |
| `pnpm version` / `pnpm pkg` | pnpm forwards both to npm verbatim, so
these are the same code path as before — neither resolves dependencies,
so `workspace:` is inert there |
| lockfile vs `exclude-links-from-lockfile=true` | `--frozen-lockfile`
green with the new `link:../js-sdk` entry, including after a
release-style version bump |
## Everything packs and publishes with pnpm
Only pnpm rewrites `workspace:`. `npm pack` copies the protocol into the
tarball verbatim and `npm install` then refuses it. Rather than hand-pin
the range back before each npm call, every flow that produces or
installs a CLI tarball now uses pnpm:
| flow | before | after |
|---|---|---|
| `pkg_artifacts.yml` | `npm pack` | `pnpm pack` |
| `publish_candidates.yml` | `npm publish --provenance` | `pnpm publish
--provenance --no-git-checks` |
| `.github/actions/build-cli` | `npm install -g .` | `pnpm link
--global` |
The `build-cli` action was a **third** npm consumer of the manifest,
missed on the first pass. It only worked because npm symlinks a local
directory for `-g` without resolving its dependencies at all — verified:
a control package depending on `chalk` installed with exit 0 and chalk
was never fetched. Force packing (`install_links=true`) and it dies with
`EUNSUPPORTEDPROTOCOL`.
Moving the rewrite to pack time changes *when* it resolves, which
matters in `pkg_artifacts.yml`: `pnpm pack` uses whatever version the
workspace SDK has at that moment, and that job renames the SDK to an
unpublished prerelease. Packing the CLI first was required —
```console
# SDK renamed first (wrong order)
CLI packed with e2b -> ^2.36.2-fake-branch.0 # never published → ETARGET
# CLI packed first (as merged)
e2b-cli-2.16.1-fake-branch.0.tgz -> e2b: ^2.36.1 # published, resolvable
```
`publish_candidates.yml` needs the opposite order and already had it:
the SDK RC *is* published first, so the CLI correctly pins that RC.
`--no-git-checks` is new there — candidates are cut from a feature
branch with the version bump uncommitted, so `pnpm publish` would
otherwise refuse.
### Not enforced, deliberately
I went down a path here and backed out of it, so it is worth recording.
I first added a
`prepack` guard on `packages/cli` that refused to build a tarball for
any packer but
pnpm. It had three bypasses: `npm_config_user_agent` is inherited, so
npm spawned from
pnpm still reports `pnpm/…` and sailed through it; and
`--ignore-scripts` and
`npm install -g <dir>` never run lifecycle scripts at all. I then
replaced it with a
step that installed the packed tarball with npm on every PR, which did
cover all of
those (verified: it rejects an `npm pack` tarball with
`EUNSUPPORTEDPROTOCOL` while
`pnpm pack` resolves the range to `^2.36.1`).
Both are now gone, in favour of keeping this PR to its actual subject.
So the rewrite is
unverified: the existing flows all use `pnpm pack`/`pnpm publish`, and
`changeset publish` picks pnpm by detecting the workspace, so it happens
— but nothing
catches it if a future flow reaches for npm instead. The tarball-install
step is a cheap
seven lines if we later decide we want it.
## Behavior change worth knowing
CLI tests previously resolved `e2b` from `node_modules`, i.e. the
*previously released* SDK, while `tsconfig.json` and the tsdown bundle
already used `../js-sdk/src`. `vitest.config.ts` now has a matching
alias, so all three agree and tests exercise the SDK that ships. The
alias is load-bearing — without it the workspace package's `main`
(`dist/index.js`) doesn't exist until the SDK is built:
```
Error: Failed to resolve entry for package "e2b".
⎯⎯⎯⎯⎯⎯ Failed Tests 11 ⎯⎯⎯⎯⎯⎯⎯
```
A broken SDK in the tree now fails CLI tests. `cli_tests.yml` and
`pkg_artifacts.yml` already built the SDK before the CLI, so no CI
ordering changed.
## Not retagging the past
Tags up to `e2b@2.36.1` / `@e2b/cli@2.16.0` / `@e2b/python-sdk@2.35.0`
stay off by one — moving published tags breaks anyone who pinned them.
**Build those versions from the npm tarball or the PyPI sdist, not from
the git tag.** That matters for distro packagers: `python3Packages.e2b`
in nixpkgs shipped 1.5.0 as 1.5.1 for exactly this reason. This caveat
is recorded here and in [SDK-298](https://linear.app/e2b/issue/SDK-298)
rather than in the repo.
## Follow-up
SDK-298 also notes `packages/python-sdk/pyproject.toml` pins
`uv_build>=0.10.0,<0.11.0`, so packagers on uv 0.11.x must patch it to
build at all. And `e2b-dev/code-interpreter` has the same tag bug in its
own publish workflow.
> **Corrections to earlier versions of this description:**
>
> 1. It suggested demoting `e2b` to a `devDependency` since the bundle
inlines it. That breaks the CLI — but *not* for the reason given next.
> 2. It then claimed `tsdown.config.ts` derives `alwaysBundle` from
`dependencies`, so removing `e2b` makes it *external* and the CLI ships
a bare `require("e2b")`. **That is backwards.** tsdown externalizes
exactly the production dependencies (`getProductionDeps` = `dependencies
∪ peerDependencies ∪ optionalDependencies`), so listing `e2b` there is
what would externalize it; `alwaysBundle` exists to cancel that. A
devDependency is *also* inlined. Verified with a control: moving `e2b`
to `devDependencies` still emits zero `require("e2b")`, while adding it
to `excludedPackages` is what produces the bare require and drops the
bundle from 2.14 MB to 1.84 MB.
>
> The real reason it must stay a dependency is runtime resolution: the
SDK reaches `undici`, `glob` and `tar` through `dynamicImport`, which is
deliberately opaque to bundlers, so they resolve from `node_modules` at
run time. The CLI declares none of them and gets all three via `e2b`:
>
> ```
> undici: present undici8: present glob: present tar: present
> ```
>
> Without them `e2b template build` loses `glob`/`tar` and
`loadUndici()` returns `undefined`, silently downgrading every request
to the global `fetch` and giving up H2 and proxy support. So: **do not
demote `e2b` to a devDependency.** This warning lives only here — there
is no in-repo note for it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Following the dashboard's teams→projects rename (e2b-dev/dashboard#521),
this PR points the `--project` flag help at the dashboard's
`?tab=general` entrypoint (General settings, where the project ID lives)
instead of the old `?tab=team`, and adds a `@e2b/cli` patch changeset.
It also rewrites the CLI README's headless-auth note to use
`E2B_API_KEY` (the supported browserless path) instead of
`E2B_ACCESS_TOKEN`, pointing at the dashboard's API Keys tab, and drops
the now-redundant `E2B_ACCESS_TOKEN` vs `E2B_API_KEY` callout. The SDKs'
`?tab=keys` and the CLI's `?tab=personal` links stay unchanged — those
tabs remain valid entrypoints.
## Example
```
$ e2b template create --help
-t, --project <project-id> specify the project ID that the operation will be associated with.
You can find project ID in the project settings in the E2B dashboard
(https://e2b.dev/dashboard?tab=general).
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Follow-up to #1613, which fixed the `glob@11` deprecation warning in
`e2b` but left `@e2b/cli` warning via `@npmcli/package-json@5 →
glob@10`. That PR proposed `@npmcli/package-json@7`, but `^7` alone
isn't enough — 7.0.0–7.0.2 still depend on the equally deprecated
`glob@^11`, and the move to `glob@13` only landed in **7.0.4**, so this
pins `^7.0.5`. Since `@npmcli/package-json@7` requires Node `^20.17.0 ||
>=22.9.0`, the CLI's Node 22 floor moves from `>=22` to `>=22.9.0` —
matching the dependency exactly rather than excluding anyone it still
supports. Node 20 support is unchanged, since `^20.17.0` covers the
existing `>=20.18.1 <21`.
## Before / after
```console
$ npm install @e2b/cli # before
npm warn deprecated glob@11.1.0: Old versions of glob are not supported...
npm warn deprecated glob@10.5.0: Old versions of glob are not supported...
added 183 packages in 4s
$ npm install @e2b/cli # after (both tarballs packed locally)
added 145 packages in 1s
```
No API change. `e2b template init` is the only consumer, and the
`PackageJson.load`/`create`/`update`/`save` surface it uses is unchanged
across the bump.
## Verification
- Packed `e2b` + `@e2b/cli` and installed into a scratch project with
`overrides` pointing `e2b` at the local tarball (the post-release
state): zero deprecation warnings, `npm ls glob --all` reports only
`glob@13.0.6`.
- Ran `e2b template init -n my-tmpl -l typescript` from that packed
install against a real host `package.json` — scripts added, pre-existing
scripts preserved.
- `packages/cli` suite: 102 passed / 1 skipped, including all 14
`template init` tests, which assert on the written `package.json` in
both the `load` (existing file) and `create` (no file) branches.
`template/create.test.ts` fails identically on a clean tree in this
environment — it requires `E2B_API_KEY`.
- `pnpm run format` / `lint` / `typecheck` clean.
`@types/npmcli__package-json` stays at `^4.0.4`; v7 ships no types.
- `.tool-versions` is untouched: the pinned `nodejs 22.18.0` already
satisfies `>=22.9.0`, so CI (which derives `node-version` from that
file) needs no change.
Closes SDK-297. Follow-up to #1613 (SDK-296).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Fixes all 8 open [Dependabot
alerts](https://github.com/e2b-dev/E2B/security/dependabot), all in
`pnpm-lock.yaml`:
| Package | Severity | Alerts | Before | After | How |
|---|---|---|---|---|---|
| `@vitest/browser` | critical | #328 | 4.1.8 | 4.1.10 | updated the
vitest family in js-sdk and cli devDeps (4.1.10 peer-requires
`vitest@4.1.10` exactly) |
| `tar` | critical/high/medium ×4 | #324–#327 | 7.5.16 | 7.5.21 | bumped
the js-sdk runtime dep floor to `^7.5.19` + repo-wide override |
| `sharp` | high | #329 | 0.34.5 | 0.35.3 | new override (pinned exactly
by miniflare, dev-only) |
| `shell-quote` | high | #323 | 1.8.4 | 1.10.0 | widened existing
override (dev-only, via npm-run-all) |
| `brace-expansion` | high | #322 | 2.1.0 | 2.1.2 | widened existing
override |
The only runtime-dependency change is `tar` in the js-sdk (used for
template build contexts), so a patch changeset for `e2b` is included.
The CLI bundles the SDK and its dependencies into `dist/index.js`, so
the published CLI also ships the vulnerable `tar` — a patch changeset
for `@e2b/cli` is included to rebundle it. Everything else is dev
tooling or lockfile-only.
## Verification
- `pnpm run lint` and `pnpm run typecheck` pass (the 7 python-sdk ty
diagnostics pre-exist on main)
- js-sdk: unit + connectionConfig (393 passed) and template projects
(132 passed, exercises the new `tar` end-to-end against the real API) on
vitest 4.1.10; `pnpm run build` clean
- js-sdk `test:cf` passes — miniflare/workerd boots with sharp 0.35.3
- cli: full suite green (103 passed) on vitest 4.1.10
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## What happened
Release run
[30006966441](https://github.com/e2b-dev/E2B/actions/runs/30006966441)
successfully published **e2b@2.35.3** and **@e2b/cli@2.15.0** to npm and
pushed both tags, but then failed on the **Update lock file** step:
`pnpm i` ran ~6 seconds after `npm publish` and the registry had not
propagated the new version yet (`ERR_PNPM_NO_MATCHING_VERSION: No
matching version found for e2b@^2.35.3 — the latest release of e2b is
"2.35.2"`). Because that step failed, the **Commit new versions** step
was skipped, leaving main with stale versions and unconsumed changesets.
Auditing the rest of the publish path for similar races also turned up a
long-dead step: the `@e2b/sdk` alias republish.
## Changes
**Commit 1 — replay the missing release commit.** Reproduces exactly
what the bot would have committed: `pnpm run version` (consumes the
three changesets, bumps js-sdk 2.35.2 → 2.35.3 and cli 2.14.0 → 2.15.0)
followed by `pnpm i --no-link --no-frozen-lockfile` (now succeeds — the
registry has long since propagated). The only commit that landed on main
after the release was dispatched
([e334c87](https://github.com/e2b-dev/E2B/commit/e334c87f8fc60be56cc5970d6f6399331242bace))
touches only `.github/`, so per the workflow's own safety rule the
version bump is safe to apply on top: the published artifacts match the
source.
**Commit 2 — prevent recurrence.** The `Update lock file` step in
`publish_packages.yml` now retries with exponential backoff
(10/20/40/80/160s, up to ~5 min total) before failing, since the npm
registry is eventually consistent and this race will recur on any
release where propagation takes more than a few seconds.
**Commit 3 — remove the dead `@e2b/sdk` alias republish.**
`packages/js-sdk/scripts/post-publish.sh` republished each release under
the deprecated `@e2b/sdk` name and immediately re-deprecated it. It has
silently failed on every release since 2.5.0 (2025-10-28): the CI npm
token lacks publish rights to `@e2b/sdk` (`E404` on `PUT
https://registry.npmjs.org/@e2b%2fsdk`, npm's masking of 403) and the
`|| true` swallowed the error — visible in this run's log right before
the lockfile failure. All published `@e2b/sdk` versions already carry
the "renamed to e2b" deprecation notice, which is the coherent end
state; resuming alias publishes would only reward not migrating. The
script and its `postPublish` hook are deleted (the root `pnpm run -r
postPublish` stays — python-sdk still uses its hook for PyPI). No
changeset: nothing in the published artifact's runtime changes, and the
alias hasn't published in 9 months so user-visible behavior is
unchanged.
## Notes
- Please merge before the next release: until then main still claims
2.35.2/2.14.0, and a future `changeset version` run would compute wrong
bumps from the stale base.
- The version-bump commit intentionally consumes the existing three
changesets.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bumps `~/.e2b/config.json` to `version: 2` and renames
`teamName`/`teamId`/`teamApiKey` to
`projectName`/`projectId`/`projectApiKey`. The rename is internal to the
config file format — all user-facing CLI output, flags (`--team`), and
env vars (`E2B_TEAM_ID`) still say "team", and API `teamID` parameters
are unchanged.
Existing v1 configs keep working: they are converted to the new format
in memory on read, and the file on disk is left untouched — the v2
format is only persisted through paths that write the config anyway
(login, `e2b auth configure`, token refresh), so older CLI versions can
still read the file in the meantime. Unrecognized configs are no longer
deleted either; the CLI treats them as signed out and `e2b auth login`
overwrites them. Tools that read the config file directly must handle
the new field names once the file is written in the v2 format.
## Usage
```jsonc
// ~/.e2b/config.json (fresh login, or any config write after upgrading)
{
"version": 2,
"projectName": "default",
"projectId": "team-id",
"projectApiKey": "e2b_...",
// identity, oauth, tokens, last_refresh unchanged
}
```
CLI output is unchanged:
```bash
$ e2b auth info
You are logged in as user@example.com,
Selected team: default (team-id)
```
## Testing
`user_config_migration.test.ts` covers in-memory v1→v2 migration, v2
pass-through, and unrecognized configs being treated as signed out
without deleting the file; existing config-permissions and backend
integration tests updated to the new fields. `format`, `lint`,
`typecheck`, `build`, and `pnpm run test` pass (backend integration
suites are environment-gated on credentials).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Renames the `--team` flag to `-t, --project` on `template list`,
`template publish`, `template unpublish`, and `template delete`.
`--team` keeps working as a hidden alias that prints a deprecation
warning to stderr. The project ID can now also be set via the new
`E2B_PROJECT_ID` environment variable, with `E2B_TEAM_ID` still
supported as a fallback. Resolution precedence: `--project` > `--team` >
`E2B_PROJECT_ID` > `E2B_TEAM_ID` > `~/.e2b/config.json`.
## Usage
```sh
e2b template list --project <project-id> # new flag (also -t)
e2b template list --team <project-id> # still works, warns: "The --team flag is deprecated, use --project instead."
E2B_PROJECT_ID=<project-id> e2b template list # new env var
E2B_TEAM_ID=<project-id> e2b template list # still supported
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
The CLI no longer writes `e2b.toml` anywhere, so this removes the dead
code around it:
- `saveConfig` and its `getConfigHeader` helper in
`packages/cli/src/config/index.ts` had zero callers — removed along with
now-unused imports.
- The `team_id` field is dropped from the config schema and the unused
`localConfigTeamId` parameter from `resolveTeamId` — nothing consumed it
since the legacy `template build` command was removed. Team resolution
is now: `--team` flag → `E2B_TEAM_ID` env → `~/.e2b/config.json` (the
last only when `E2B_API_KEY` isn't set). yup ignores unknown keys, so
legacy tomls containing `team_id` still parse.
Parsing (`loadConfig`, `deleteConfig`, `getConfigPath`) is intentionally
kept as the backward-compatibility read path for legacy projects:
`template migrate` (its whole purpose), `template publish`, `template
delete`, and `sandbox create`. No user-facing behavior changes; includes
a `@e2b/cli` patch changeset.
## Test
Format, lint, and typecheck pass; CLI tests: 88 passed, 8 skipped (one
pre-existing backend integration suite fails only due to missing
`E2B_API_KEY` in the environment).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Copy-only rename of the remaining user-visible "team" strings to
"project" in the CLI (EN-1891) — part of the Teams → Projects rename,
following the dashboard copy pass. 12 string literals across `auth
login`, `auth info`, `auth configure`, and `template publish`; no flag,
env var, config key, API call, or exit-code behavior changes.
Explicitly untouched (owned by other PRs): `--team` flag + help text
(#1571), `~/.e2b/config.json` keys (#1570), `e2b.toml` `team_id`
(#1569), internal identifiers and API `Team` types. Best merged after
#1569–#1571 to keep their rebases trivial.
## Usage examples
```
$ e2b auth login
Logged in as you@e2b.dev with selected project Your Project
$ e2b auth info
You are logged in as you@e2b.dev,
Selected project: Your Project (a1b2c3d4)
$ e2b auth configure
? Select project
Your Project (a1b2c3d4) (currently selected project)
Project Your Project (a1b2c3d4) selected.
$ e2b template publish
⚠️ This will make the template public to everyone outside your project
```
## Testing
- No new tests — strings only, not functionality (per review). Existing
suite passes except the pre-existing backend-integration suites that
need live sandbox access (fail identically on main).
- Patch changeset included.
## Description
Removes the `e2b-cli-command/<command>` token (added in #1544) from the
CLI's User-Agent integration attribution, so CLI traffic is attributed
only by tool and version. This also lets `connectionConfig` and `client`
in `packages/cli/src/api.ts` go back to plain `const` exports, deleting
the per-command config/client rebuild machinery and the `preAction` hook
that drove it. The attribution test now only checks the SDK and CLI
tags, and a patch changeset for `@e2b/cli` is included.
User-Agent sent by `e2b sandbox list`, before and after:
```
before: e2b-js-sdk/2.9.0 (Node.js/22.11.0) e2b-cli/2.13.3 e2b-cli-command/sandbox.list
after: e2b-js-sdk/2.9.0 (Node.js/22.11.0) e2b-cli/2.13.3
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Follow-up promised in #1524 (original attempt #1525, closed while
blocked on the backend User-Agent parser, since fixed by
e2b-dev/infra#3149, which now iterates User-Agent tokens and ignores
unrecognized ones — so the extra tags are safe on template builds).
Sets `ConnectionConfig.setIntegration('e2b-cli/<version>')` at the top
of `src/api.ts` before the shared connection config is built at import
time, and a commander `preAction` hook extends the tag with the
canonical invoked command (alias `ls` reports as `list`), rebuilding the
shared config and client since they capture the User-Agent at
construction. Every CLI request then carries:
```
User-Agent: e2b-js-sdk/2.32.0 e2b-cli/2.13.1 e2b-cli-command/sandbox.list
```
Tests drive the built CLI (`sandbox list` and the `ls` alias) against a
local stub API server and assert the received User-Agent, which also
guards that the bundle keeps shipping the workspace SDK where
`setIntegration` exists. Includes a patch changeset for `@e2b/cli`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes two small CLI issues. The `e2b template create --memory-mb` help
text claimed a default of 512 MB, but the real default is 1024 MB — the
help now reflects that. The `e2b sandbox pause` command was calling the
deprecated `Sandbox.betaPause()` alias and now calls `Sandbox.pause()`
directly.
## Usage
```sh
e2b template create --help # --memory-mb now shows "The default value is 1024."
e2b sandbox pause <sandboxID> # behaves the same, no longer uses the deprecated method
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersedes #1516 (same modernization at TypeScript 6.0). Rebased onto
`main` now that the build runs on **tsdown** (#1515).
## What & why
Adopt **TypeScript 7** for both packages and modernize the compiler
config.
TypeScript 7.0's native compiler [ships no programmatic API
yet](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0)
(it lands in 7.1), so anything built on the TS compiler API breaks on it
— here that's tsdown's `.d.ts` generation and the codegen scripts
(`openapi-typescript`, `json-schema-to-typescript`). Per the official
guidance, TS 7 is installed **side-by-side** with TS 6:
```json
"@typescript/native": "npm:typescript@^7.0.2", // native tsc — used for type-checking
"typescript": "npm:@typescript/typescript6@^6.0.2" // TS6 w/ compiler API — used by tooling
```
- `tsc --noEmit` (typecheck) → **native TypeScript 7.0.2** (verified:
`tsc --version` → 7.0.2)
- `import 'typescript'` → **TypeScript 6.0** *with* the compiler API →
tsdown dts + codegen keep working
- Bonus: tsdown's dts no longer prints the "TypeScript 7.0 does not yet
have a stable API and is experimental" warning (it's on the 6.0 API now)
**Internal build-config change only — no public API or runtime behavior
changes.**
## Compiler options: before → after
### `packages/js-sdk/tsconfig.json`
| option | before | after |
|---|---|---|
| `target` | `es6` | `es2022` |
| `lib` | `["dom","ESNext"]` | `["dom","es2022"]` |
| `module` | _(unset)_ | `esnext` |
| `moduleResolution` | `node` | `bundler` |
| `allowJs` | `true` | **removed** (no `.js` sources) |
| `allowSyntheticDefaultImports` | `true` | **removed** (implied by
`esModuleInterop`) |
| `useDefineForClassFields` | _(false, implied by es6)_ | **`false` (now
explicit)** — see note |
### `packages/cli/tsconfig.json`
| option | before | after |
|---|---|---|
| `moduleResolution` | `node` | `bundler` |
| `strictNullChecks`, `strictFunctionTypes`, `strictBindCallApply`,
`strictPropertyInitialization`, `noImplicitThis`, `alwaysStrict` |
`true` | **removed** (implied by `strict`) |
| `downlevelIteration` | `true` | **removed** (removed in TS 7; no-op at
`es2022`) |
| `baseUrl` | `"."` | **removed** (removed in TS 7) |
| `paths` | `{ e2b }` | `{ src, "src/*", e2b }` (replaces `baseUrl` for
the existing `src/...` import style) |
| `outDir` | `"dist"` | **removed** (unused under `tsc --noEmit`) |
| `exclude` | _(none)_ | `["dist","node_modules"]` (so the built bundle
is never type-checked) |
`target`/`lib` for the CLI were already `es2022`.
## Notes / decisions
- **Why side-by-side, not a plain `typescript@7` bump:** TS 7.0 is the
native (Go) compiler rewrite — feature-identical to 6.0 for
type-checking, no programmatic API until 7.1. A plain bump crashed both
codegen tools (`Cannot read properties of undefined (reading
'createKeywordTypeNode')`). Side-by-side gives native-TS-7 checking
while keeping the TS-6 API for tooling. Once 7.1 ships the API and the
tools update, this collapses back to a single `typescript@7` dep.
- **`useDefineForClassFields: false` is pinned explicitly.** Raising
js-sdk's `target` to `es2022` flips this default to `true`, changing
class-field emit and shifting stack frames. The template builder
resolves the caller's directory and per-step traces via **fixed-depth**
stack walking (`getCallerDirectory` in `src/template/index.ts`), so the
extra frames threw it off by one — resolving `.copy('folder/*', …)`
against the wrong base dir and mis-attributing build steps
(`tests/template/build.test.ts` + `stacktrace.test.ts`). Pinning `false`
keeps the exact pre-existing field semantics (es6 already implied
`false`); adopting `define` semantics should be a separate, deliberately
tested change.
- **Target stays at `es2022`, not `es2023`.** `engines` still allow Node
20 (`>=20.18.1 <21 || >=22`).
- **`moduleResolution: "bundler"`** typechecks + builds cleanly in both
packages. The CLI's `baseUrl`-based bare imports (`from 'src/user'`,
`from 'src'`) are preserved via `paths`; the bundled output still
resolves them (build verified, binary smoke-tested).
## Not done (intentionally)
- **`verbatimModuleSyntax`** — ~177 `import type` conversions; left as a
follow-up.
- **Shared `tsconfig.base.json`** — the two configs diverge too much to
factor out cleanly.
## Verification
- `pnpm run typecheck` ✅ both packages, on **native TS 7.0.2**
- `pnpm run build` ✅ both packages (js-sdk ESM + CJS + **DTS**; cli CJS;
binary smoke-tested)
- codegen ✅ `openapi-typescript` + `json2ts` run and produce identical
output (idempotent)
- `pnpm run lint` ✅ both packages
- `pnpm run test` — `template/build` + `template/stacktrace` now pass
(`stacktrace` verified locally 30/30); remaining local failures are all
`E2B_API_KEY`-gated live tests, unaffected by this change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switches the build tooling for `packages/js-sdk` and `packages/cli` from
`tsup` (esbuild) to `tsdown` (rolldown), replacing each `tsup.config.js`
with a `tsdown.config.ts` and updating the `build`/`dev` scripts and
devDependencies. The published artifact layout is intentionally
unchanged — the SDK still ships `dist/index.js` (CJS), `dist/index.mjs`
(ESM) and `dist/index.d.ts`/`.d.mts`, and the CLI still ships an
executable `dist/index.js` plus `dist/templates` — kept identical via
`fixedExtension: false`. CLI dependency bundling is preserved by mapping
the old `noExternal` to tsdown's `deps.alwaysBundle` (still excluding
the ESM-only, dynamically-imported `inquirer`), and template copying
moves from an `onSuccess` shell step to tsdown's `copy` option.
Also aligns Node versions: `engines.node` for both packages is set to
`20 || >=22`, the CLI build targets `node20`, and the pinned `nodejs` in
`.tool-versions` is bumped to `22.11.0`. The large `pnpm-lock.yaml` diff
is expected — it swaps the tsup/esbuild dependency tree for tsdown's
rolldown tree (no lockfile format change).
## Verification
- Both packages build cleanly with output filenames identical to the
previous tsup builds.
- `typecheck`, `lint` (oxlint) and `build` pass for both packages; the
built CLI runs (`--version`).
- Built js-sdk imports correctly in both CJS (`require`) and ESM
(`import`), exposing the default `Sandbox` export and all named exports.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces ESLint (and its `@typescript-eslint/*` and `unused-imports`
plugins) with [oxlint](https://oxc.rs) across the `js-sdk` and `cli`
packages. A root `.oxlintrc.json` replaces the three `.eslintrc.cjs`
files, the package `lint` scripts now run `oxlint`, the related
devDependencies are swapped for `oxlint`, and the lint CI path filter is
updated accordingly. Formatting rules
(`quotes`/`semi`/`linebreak-style`) are dropped because Prettier already
enforces them, and `no-unused-vars` is set to error to preserve the
previous unused-imports check. The one behavior change is that
`@typescript-eslint/member-ordering` has no oxlint equivalent and is no
longer enforced. `lint`, `typecheck`, and `prettier` all pass clean for
both packages.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CLI declared `dockerfile-ast` as a dependency but never imported it
— all Dockerfile parsing in the CLI goes through the `e2b` SDK, which
keeps its own (newer) `dockerfile-ast` dependency. This drops the
redundant copy from `packages/cli/package.json`, removing
`dockerfile-ast@0.6.1` and its sub-deps from the lockfile while
`dockerfile-ast@0.7.1` (used by the js-sdk) stays. No behavior change;
CLI typecheck and lint pass, and a `@e2b/cli` patch changeset is
included.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the generated Python build scripts to use an absolute import (from
template import template) instead of a relative one, which broke python
build_dev.py with ImportError: attempted relative import with no known
parent package since the files are emitted as flat siblings with no
package. This reverts an unintended change from #954 that was flagged by
Cursor Bugbot at the time but not addressed. Fixes#1477.
Adds override flags to `e2b template migrate` so the generated SDK files
don't have to inherit everything from `e2b.toml`: `--name`/`-n`
(template name), `--cmd`/`-c` (start command), `--ready-cmd` (ready
command), `--cpu-count`, and `--memory-mb`. Each flag falls back to the
corresponding config value when omitted, and `--memory-mb` is validated
to be even. Includes tests covering the overrides and the odd-memory
rejection, plus a changeset for `@e2b/cli`.
## Usage
```bash
e2b template migrate \
--language typescript \
--name my-custom-name \
--cmd "node server.js" \
--ready-cmd "curl localhost:3000" \
--cpu-count 4 \
--memory-mb 2048
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Restructures the CLI config schema to v1 with nested , , and sections.
Replaces the legacy e2b access token auth with a Hydra OAuth flow using
refresh tokens. Token expiry is decoded from the JWT claim at runtime
instead of being stored.
## Changes
- **New config schema (v1)**: , , , , , (ISO timestamp)
- **Token refresh**: decodes from the JWT access token, refreshes via
Hydra when expired, writes only (not )
- **Deprecated config handling**: Old flat configs without are deleted
with a re-login prompt. No migration path — users re-authenticate.
- ****: Set on and , not on token refresh
- ****: New helper for direct error throwing without type narrowing;
auth commands use it instead of
- **Type-safe team responses**: Removed casts, use type extraction
- **Logout**: Revokes refresh token via Hydra before deleting config;
fixed crash when deprecated config already deleted by
- **Removed**: Token expiry display from , from
## Config example
```json
{
"version": 1,
"identity": { "email": "user@example.com" },
"oauth": { "token_endpoint": "https://hydra.../oauth2/token", "client_id": "..." },
"tokens": { "access_token": "...", "refresh_token": "..." },
"last_refresh": "2024-06-24T12:00:00.000Z",
"teamName": "...", "teamId": "...", "teamApiKey": "..."
}
```
## Test plan
- [x] `pnpm run typecheck` passes
- [x] `pnpm exec eslint` passes on changed files
- [x] `pnpm exec prettier --check` passes
- [x] `pnpm exec vitest run tests/user_config_permissions.test.ts`
passes
- [ ] Manual: `e2b auth login` writes v1 config
- [ ] Manual: token refresh via `e2b auth configure` with expired JWT
- [ ] Manual: old flat config triggers deprecation and re-login
Depends on: dashboard PR adding the Hydra OAuth CLI flow
---------
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
## Why
The Release run
[27844631141](https://github.com/e2b-dev/E2B/actions/runs/27844631141/job/82412481993)
**published all packages successfully** but then failed at the final
*"Commit new versions"* step — the version-bump commit-back to \`main\`
was rejected as non-fast-forward (another PR landed on \`main\` during
the release window).
As a result the registries are ahead of the repo:
| Package | Published | Repo (main) before this PR |
|---|---|---|
| \`e2b\` (JS) | 2.30.4 (npm) | 2.30.3 |
| \`@e2b/cli\` | 2.12.2 (npm) | 2.12.1 |
| \`e2b\` (Python) | 2.29.4 (PyPI) | 2.29.3 |
The changeset \`fix-logo-pypi-npm.md\` was also never consumed and is
still on \`main\`.
## What this PR does
Replays exactly what the failed *"Commit new versions"* step would have
committed — i.e. \`pnpm run version\` (changeset version +
\`postVersion\` poetry sync) + lockfile update:
- Bumps \`e2b\` → 2.30.4, \`@e2b/cli\` → 2.12.2, \`@e2b/python-sdk\` →
2.29.4 (matching what's already published)
- Deletes the consumed changeset \`fix-logo-pypi-npm.md\`
- Updates \`pnpm-lock.yaml\` (CLI's \`e2b\` dep → 2.30.4)
No new packages are published by merging this — it only syncs the repo
to the registries. **Do not re-run the Release workflow** for this
changeset; the versions already exist on npm/PyPI.
## Summary
Fixes the duplicate logo issue on NPM and PyPI caused by #1462. The
`#gh-light-mode-only` / `#gh-dark-mode-only` URL fragments are
GitHub-specific — NPM and PyPI ignore them and render both `<img>` tags.
Switches all three package READMEs (CLI, JS SDK, Python SDK) to
`<picture>` elements:
```html
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".../logo-white.png">
<source media="(prefers-color-scheme: light)" srcset=".../logo-black.png">
<img alt="E2B Logo" src=".../logo-black.png" width="200">
</picture>
```
- **GitHub**: `<picture>` + `prefers-color-scheme` handles theme
switching
- **NPM/PyPI**: `<picture>` not supported, falls back to the single
`<img>` (black logo)
Link to Devin session:
https://app.devin.ai/sessions/4983f23d23934d2c9a51733f5f9920f3
Requested by: @mlejva
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: vasek <vasek.mlejnsky@gmail.com>
## Summary
Replace the old `logo-circle.png` in the CLI, JS SDK, and Python SDK
READMEs with the new E2B wordmark logos that adapt to GitHub's theme
setting.
Each package README now uses a `<picture>` element:
```html
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".../logo-white.png">
<source media="(prefers-color-scheme: light)" srcset=".../logo-black.png">
<img alt="E2B Logo" src=".../logo-black.png" width="200">
</picture>
```
- **Light theme** → black logo (`logo-black.png`)
- **Dark theme** → white logo (`logo-white.png`)
- **NPM/PyPI** (no `<picture>` support) → falls back to the black logo
via the `<img>` tag
New logo assets added to `readme-assets/`: `logo-black.png`,
`logo-white.png`.
Includes a patch changeset for `@e2b/cli`, `e2b` (JS SDK), and
`@e2b/python-sdk`.
Link to Devin session:
https://app.devin.ai/sessions/4983f23d23934d2c9a51733f5f9920f3
Requested by: @mlejva
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: vasek <vasek.mlejnsky@gmail.com>
## Summary
The access token was only ever used by the CLI, never by any SDK
operation — sandbox, template, and volume calls all authenticate with
the API key. This cleans up the auth plumbing and **deprecates** (rather
than removes) the access token on `ConnectionConfig`, so there's no
breaking change for direct SDK consumers.
## Changes
- **Deprecated** the `accessToken` (JS) / `access_token` (Python) option
on `ConnectionConfig`. It still works exactly as before — when set (or
via `E2B_ACCESS_TOKEN`) the `Authorization: Bearer` header is still sent
— but `apiHeaders` is now the recommended way to pass custom auth.
- **Clear error when the API key is missing**, pointing to the API Keys
tab (`https://e2b.dev/dashboard?tab=keys`). In JS this is gated by a
`requireApiKey` option (default `true`) so callers that authenticate
differently — like the CLI hitting `/teams` with an access token — can
opt out; in Python the API key is always required.
- Removed the unused access-token toggle from the API clients:
`requireAccessToken` (JS) / `require_access_token` (Python). No caller
ever set it to a non-default value, so behavior is unchanged.
- The CLI now passes the access token to the `/teams` endpoint via
`apiHeaders` instead of the deprecated option, and opts out of the
API-key requirement on its own clients.
- Decoupled the sandbox-scoped envd access token from
`ConnectionConfig`: `EnvdApiClient` now owns its own `envdAccessToken`
field and sets the `X-Access-Token` header itself, removing a redundant
manually-set header.
## Recommended usage
```ts
// Deprecated
new ConnectionConfig({ accessToken: 'my-token' })
// Preferred
new ConnectionConfig({ apiHeaders: { Authorization: 'Bearer my-token' } })
```
```python
# Deprecated
ConnectionConfig(access_token="my-token")
# Preferred
ConnectionConfig(api_headers={"Authorization": "Bearer my-token"})
```
## Verification
`pnpm run typecheck`, `pnpm run lint`, Python `make typecheck`, and the
unit tests all pass — including new tests for the API-key requirement
(and its opt-out) in both SDKs. Confirmed the `Authorization: Bearer`
header is still sent for both the deprecated option and
`E2B_ACCESS_TOKEN`.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
`E2B_ACCESS_TOKEN` is deprecated, so CLI commands whose endpoints accept
either credential now authenticate with `E2B_API_KEY` instead of
requiring an access token.
- `e2b template list` now uses `ensureAPIKey()`. The underlying `GET
/templates` endpoint accepts both `ApiKeyAuth` and `AccessTokenAuth`,
and since the access token is deprecated we standardize on the API key.
- `e2b template create` no longer calls `ensureAccessToken()`. Its only
API calls — `POST /v3/templates` and `POST
/v2/templates/{id}/builds/{bid}` (via the SDK's `Template.build`) —
accept only `ApiKeyAuth`, so requiring an access token locked out
API-key-only environments for no reason.
- `e2b template build` is intentionally left as-is: its v1 endpoints and
docker-registry login are access-token-only at the API level.
- Removed the now-unused `ensureAccessTokenOrAPIKey()` helper and the
`'BOTH'` variant of the auth-error box that an earlier iteration of this
PR introduced.
- Adds a real backend-integration test in
`tests/commands/template/create.test.ts` that mirrors the existing
`backend_integration.test.ts` pattern: use the real `E2B_DOMAIN` and
assert end-to-end that `template create` succeeds with only
`E2B_API_KEY` set (no `E2B_ACCESS_TOKEN`). Uses a unique template name
per run and cleans up the created template in `afterAll`.
## Test plan
- [x] `pnpm --filter @e2b/cli run typecheck`
- [x] `pnpm --filter @e2b/cli run lint`
- [x] `pnpm --filter @e2b/cli run format`
- [x] `pnpm --filter @e2b/cli run test` (local, with real `E2B_API_KEY`
— new test passes; create succeeds without `E2B_ACCESS_TOKEN`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Aligns several behavioral and API-surface discrepancies between the JS
and Python SDKs found during a cross-SDK audit. **Python:**
`commands.send_stdin`/`CommandHandle.send_stdin` now accept `bytes`
(plus `request_timeout` on the handle), `git.reset` gets a typed
`GitResetMode` with JS-matching validation, `sandbox_url` is threaded
through `get_api_params` (and the dead `SandboxOpts` key removed), and
`from_image` requires both `username` and `password` when credentials
are given. **JS:** `getFullInfo` was removed in favor of a single
`getInfo` that now includes `sandboxDomain` (matching Python's
`get_info`), `fromImage` requires both credentials, `getBuildStatus`
defaults `logsOffset` to `0`, `getMetrics`/`kill` short-circuit
consistently in debug mode (instance + static), and `requestTimeoutMs:
0` explicitly disables the request timeout. Tests were added on both
sides (git-arg validation, stdin bytes, credential validation,
timeout-0, connection config) and the CLI's `sandbox info` now uses
`getInfo`. See the changeset for the full per-SDK list.
## Usage examples
```ts
// JS: registry credentials now require both fields
Template().fromImage('registry.example.com/img:latest', { username: 'u', password: 'p' })
// JS: getInfo now exposes sandboxDomain (getFullInfo removed)
const info = await Sandbox.getInfo(sandboxId)
console.log(info.sandboxDomain)
// JS: disable the request timeout
await Sandbox.create({ requestTimeoutMs: 0 })
```
```python
# Python: send raw bytes to stdin
sandbox.commands.send_stdin(cmd.pid, b"hello")
# Python: typed git reset mode (validated)
sandbox.git.reset(repo, mode="hard")
# Python: registry credentials require both fields
Template().from_image("registry.example.com/img:latest", username="u", password="p")
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Removes the `ensureAccessToken()` call (and its now-unused import) from
`e2b template create`. The command authenticates solely via the API key
(`ensureAPIKey()`), so the access-token check was redundant.
## Changes
- Drop `ensureAccessToken` import and call in
`packages/cli/src/commands/template/create.ts`.
- Add a patch changeset for `@e2b/cli`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
- Strips all v1 build logic from \`e2b template build\` (\`bd\`): Docker
build/push, API calls, config-loading, and retry/proxy handling are
removed
- The command now only displays the existing yellow deprecation warning
(pointing to the v2 migration guide) and exits with code 1
- Deletes \`buildWithProxy.ts\` which is no longer referenced anywhere
- Moves \`getDockerfile\` helper (used by \`template create\` and
\`template migrate\`) to a new shared \`dockerfile.ts\` module, leaving
\`build.ts\` as a clean stub
## Test plan
- [ ] Run \`e2b template build\` — confirm deprecation warning is shown
and the command exits immediately
- [ ] Run \`e2b template create\` and \`e2b template migrate\` — confirm
they still work (both use the moved \`getDockerfile\` helper)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes internal symbols with zero references, found via knip
(js-sdk/cli) and vulture (python-sdk) and verified with repo-wide greps:
`wait` (js-sdk),
`asSandboxTemplate`/`asHeadline`/`selectOption`/`basicDockerfile` (cli),
and `format_execution_timeout_error` (python-sdk). No public API changes
— only dead, unexported-from-index or unreferenced code is dropped.
`format`, `lint`, and `typecheck` pass for all touched packages, and a
patch changeset is included for the three published packages.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
The CLI stores credentials (E2B access token and team API key) in
plaintext at `~/.e2b/config.json`. Today the file is created with the
process default umask, which on most Linux distributions and macOS
results in mode `0644` — readable by every other local user and by any
process running as a different UID on the same machine.
This PR routes all three write sites through a single
`writeUserConfig()` helper that creates `~/.e2b` as `0700` and
`config.json` as `0600`, matching the convention used by the AWS CLI
(`~/.aws/credentials`), `kubectl` (`~/.kube/config`), and `gh`
(`~/.config/gh/hosts.yml`).
- **CWE:** CWE-312 (Cleartext Storage of Sensitive Information) —
partial mitigation. The file remains plaintext on disk (the existing `//
TODO` in `user.ts` already acknowledges that keychain storage is the
proper long-term fix); this change reduces exposure to other local users
/ less-privileged processes, which is the standard industry mitigation
while plaintext storage remains.
- **Affected file:** `packages/cli/src/user.ts` and the three writers in
`packages/cli/src/commands/`.
- **Severity:** Moderate on shared / multi-user machines (CI runners,
dev VMs, jump boxes); low on single-user workstations.
## What's in `~/.e2b/config.json`
```ts
{
email, accessToken, // user access token
teamName, teamId, teamApiKey // team API key
}
```
`accessToken` authenticates the user against the E2B control plane;
`teamApiKey` authorizes sandbox creation against the team. Either is
sufficient to impersonate the user / spend on the team's account.
## Fix
A new helper in `packages/cli/src/user.ts`:
```ts
export function writeUserConfig(configPath: string, config: UserConfig): void {
const dir = path.dirname(configPath)
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
fs.chmodSync(dir, 0o700)
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 })
fs.chmodSync(configPath, 0o600)
}
```
The explicit `chmodSync` calls are intentional: `mkdirSync({ mode })`
and `writeFileSync({ mode })` only set permissions when the path is
created. If the directory or file already exists with looser permissions
(the common case for users upgrading), `chmodSync` corrects them on the
next write.
Call sites updated:
- `packages/cli/src/commands/auth/login.ts`
- `packages/cli/src/commands/auth/configure.ts`
- `packages/cli/src/commands/template/buildWithProxy.ts`
`logout` uses `unlinkSync` and is unaffected. I grep'd the package for
any other writers to `USER_CONFIG_PATH` — these three are the complete
set.
Behavior on Windows: `chmodSync` only manipulates the read-only bit on
Windows, which is consistent with how the AWS/kubectl/gh CLIs behave.
ACL hardening on Windows is out of scope for this change.
## Tests
Added `packages/cli/tests/user_config_permissions.test.ts`, which writes
a config to a temporary path and asserts the resulting directory is
`0700` and file is `0600`, plus that the JSON round-trips correctly.
Manually verified before/after on Linux:
```
# before this PR
$ ls -l ~/.e2b/config.json
-rw-r--r-- 1 user user 234 ... config.json
# after
$ ls -l ~/.e2b/config.json
-rw------- 1 user user 234 ... config.json
```
## Why this is worth fixing
The exploitable scenario is a multi-tenant or shared-account host:
another local user (or a process running as `nobody`, a CI worker UID, a
sandboxed app, etc.) can `cat ~/<victim>/.e2b/config.json` and lift live
credentials. No privilege escalation, no race, no special tooling — the
file is simply world-readable today.
Before submitting, I tried to disprove the finding: I checked whether
E2B sets a restrictive umask anywhere in the CLI bootstrap (it doesn't),
whether the tokens are short-lived enough to make disclosure low-impact
(the access token isn't visibly rotated and the team API key is
long-lived), and whether the directory itself was being created
restrictively elsewhere (it wasn't — `mkdirSync` was called with default
mode). None of those mitigations are in place, so the permission
tightening is doing real work.
This doesn't close out CWE-312 — that requires moving the secrets out of
plaintext entirely, which the existing TODO acknowledges. It does close
the "any local user can read it" gap, which is the cheap, high-value
half of the mitigation.
_Submitted by Sebastion — autonomous open-source security research from
[Foundation Machines](https://foundationmachines.ai). Free for public
repos via the [Sebastion AI GitHub
App](https://github.com/marketplace/sebastion-ai)._
---------
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>