A tiny Next.js chat UI for the
claude-code-sandbox-blaxel
sandbox. Built with assistant-ui.
The chatbot is a thin client: it gets a private preview to your already-deployed sandbox and proxies messages through. Provider auth (Anthropic / Bedrock) lives in the sandbox repo, not here.
⚠️ This template requires theclaude-code-sandbox-blaxelsandbox to be deployed to Blaxel first. The chatbot is a thin client — it does not bundle the sandbox. Deploying this template alone will not give you a working app.
- Install Blaxel CLI and log in:
bl login YOUR-WORKSPACE
- Deploy the sandbox once, with your provider creds:
git clone https://github.com/pipelex/claude-code-sandbox-blaxel.git cd claude-code-sandbox-blaxel cp .env.sample .env # fill ANTHROPIC_API_KEY (or Bedrock vars) bl deploy
⚠️ Do not commit.env— it contains your API key. Confirm it's in.gitignorebefore you push.
npm install
bl serve --hotreloadOpen http://127.0.0.1:1338.
bl deployThat deploys only the chatbot — it assumes the sandbox is already deployed (see Prerequisites).
If you're standing this up for the first time, run the convenience script:
./scripts/deploy-all.shIt clones claude-code-sandbox-blaxel
next to this repo (if missing), deploys the sandbox, then deploys the
chatbot. Override the sandbox location with SANDBOX_DIR=..., or skip
the sandbox step with SKIP_SANDBOX=1. The script refuses to deploy
the sandbox if its .env (with ANTHROPIC_API_KEY) is missing.
The sandbox is intentionally a separate git repo, not vendored here. One sandbox can back multiple chatbot deployments; upgrading the sandbox image doesn't force a chatbot redeploy.
Out of the box, the deployed chatbot is gated by Blaxel platform
auth — hitting its URL without a Blaxel bearer token returns 401.
This is the secure default and the right choice for a private demo or
an internal tool.
If you want anyone with the link to be able to use it, opt into public access explicitly — you accept the token-burn risk in exchange for the convenience:
- Add
public = trueat the top ofblaxel.toml. - Set
PUBLIC_ACCESS=1in the chatbot's env (Blaxel dashboard → Environment). This activates the IP-based rate limiter and the stricter per-session message cap; without it the limiter assumes you're behind auth and doesn't trustx-forwarded-forheaders. - Redeploy:
bl deploy.
Rate-limit knobs (env vars, all have safe defaults):
| Variable | Default (public) | Default (gated) | Purpose |
|---|---|---|---|
RATE_LIMIT_MESSAGES_PER_SESSION |
30 |
500 |
Lifetime message cap per sessionId. |
RATE_LIMIT_MESSAGES_PER_IP |
60 |
1000 |
Rolling-window cap per source IP. |
RATE_LIMIT_IP_WINDOW_MS |
3600000 (1h) |
same | Window length for the per-IP cap. |
MAX_CONTENT_BYTES |
16384 |
same | Hard cap on a single message body. |
When a limit trips, the API returns 429 with a structured
{ error: { code, message, retryAfterSeconds? } } body and a
Retry-After header where applicable; the UI renders a friendly
message instead of a bare status code.
Single-replica only. The limiter is in-process. If you scale the chatbot to multiple replicas, swap the in-memory
Maps insrc/lib/rate-limit.tsfor a shared backend (Redis, DDB, Upstash) — the public surface (checkRateLimit) stays the same.
GET /api/health— returns200 { status: "ok" }when the sandbox is reachable and a preview can be minted;503 { status, code, message }otherwise. Useful for uptime probes and CI smoke tests.POST /api/chat—{ sessionId, content }. Streams SSE on success; returns{ error: { code, message, … } }on failure with one of the documented error codes (SANDBOX_NOT_FOUND,SESSION_MESSAGE_LIMIT,IP_MESSAGE_LIMIT,CONTENT_TOO_LARGE,SANDBOX_UNREACHABLE, …).
src/
├── app/
│ ├── page.tsx # mounts <Chat />
│ ├── layout.tsx
│ ├── globals.css
│ └── api/
│ ├── chat/route.ts # SSE proxy → sandbox, with rate limiting
│ └── health/route.ts # liveness/preflight check
├── components/Chat.tsx # assistant-ui Thread + custom adapter
├── lib/
│ ├── sandbox.ts # resolve sandbox preview + token (typed errors)
│ ├── skills.ts # push local skill files into the sandbox
│ └── rate-limit.ts # per-session + per-IP message caps
└── skills/
└── tldr/SKILL.md # demo skill — summarize anything
scripts/
└── deploy-all.sh # one-shot deploy for sandbox + chatbot
On the first chat request, every file under src/skills/ is pushed into the
sandbox via sandbox.fs.write at /home/agent/.claude/skills/<same-path>.
The Claude Agent SDK auto-discovers any folder containing SKILL.md via
settingSources: ["user", "project"] — no image rebuild, no plugin install.
Add your own skill: drop a folder under src/skills/<name>/ containing a
SKILL.md (with YAML frontmatter name: and description:), and restart.
No code changes needed — src/lib/skills.ts walks the
directory.
Try the demo: paste a long article into the chat and ask "tldr?" — the
tldr skill activates and Claude returns a tight one-sentence headline plus
3–5 supporting bullets.
⚠️ Heads up — the push overwrites existing skills in the sandbox. On the first chat request, the template pushes everything undersrc/skills/into/home/agent/.claude/skills/and replaces whatever is already there. If you previously installed skills into the same sandbox by other means (e.g.npx skills add,claude plugin install, or manualsandbox.fs.writecalls), deploying this template will wipe them — you'll be left with only the skills shipped undersrc/skills/. Move any skills you want to keep intosrc/skills/before deploying, or point the template at a different sandbox.
MIT