An OpenCode plugin that runs all AI sessions inside isolated TensorLake sandboxes. Every bash command, file read/write, and search is executed in the sandbox rather than on your local machine.
When the plugin is active, OpenCode intercepts the standard tool calls (bash, read, write, edit, ls, glob, grep) and routes them to a TensorLake sandbox.
No sandbox is created when you start OpenCode. The sandbox is provisioned lazily on the model's first tool call in a session. If you launch OpenCode and nothing seems to happen, that's expected — ask the model to run a command to spin one up.
- Sandbox lifecycle - A sandbox is created lazily on the first intercepted tool call in a session (not at launch) and deleted when the session is deleted. Sandbox state is persisted to disk so that reconnection is possible across OpenCode restarts.
- Suspension/resume - If a sandbox is found in a suspended state it is automatically resumed before use.
- System prompt injection - A block is appended to the system prompt on every request informing the model that it is operating inside a sandbox at
/tmp/workspace. - Toast notifications - Sandbox status events (created, connected, resumed, deleted) surface as TUI toasts.
- Logging - All plugin activity is written to
~/.local/share/opencode/log/tensorlake.log.
| OpenCode tool | Sandbox implementation |
|---|---|
bash |
sandbox.run('sh', { args: ['-c', cmd] }) via SDK |
read |
sandbox.readFile(path) via SDK |
write |
sandbox.writeFile(path, content) via SDK |
edit |
read + string replace + write |
ls |
sandbox.listDirectory(path) via SDK |
glob |
find … -name "pattern" via bash |
grep |
grep -rn … via bash |
- An OpenCode installation (see opencode.ai)
- A TensorLake account and API key (sign up at tensorlake.ai)
OpenCode can install plugins directly from npm. Use the npm package name in your OpenCode config unless you are developing the plugin locally.
Add the plugin package name to ~/.config/opencode/opencode.json (create it if it doesn't exist):
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"tensorlake-opencode"
]
}OpenCode treats bare plugin names as npm packages, so it will install tensorlake-opencode from the npm registry.
You can list multiple plugins in the same array:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"tensorlake-opencode",
"opencode-helicone-session",
"opencode-wakatime",
"@my-org/custom-plugin"
]
}export TENSORLAKE_API_KEY=your_api_key_hereOpenCode can also run plugins as TypeScript source files directly via its embedded Bun runtime. Use this option when you are modifying this repository locally.
Clone the repository:
git clone https://github.com/tensorlakeai/opencode-tensorlake-plugin ~/opencode-tensorlake-pluginThen point OpenCode at the plugin entry file:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"file:///Users/your-username/opencode-tensorlake-plugin/.opencode/plugin/index.ts"
]
}For local paths:
- The
file://prefix is required. Without it, OpenCode treats the value as an npm package name. - The path must point directly to the
.tsentry file (.opencode/plugin/index.ts), not the repository root.
| Variable | Default | Description |
|---|---|---|
TENSORLAKE_API_KEY |
(required) | Your TensorLake API key |
TENSORLAKE_ORGANIZATION_ID |
(required for PAT keys) | Organization ID (e.g. org_…). Required when using a Personal Access Token; not needed for project-scoped keys. |
TENSORLAKE_PROJECT_ID |
(required for PAT keys) | Project ID (e.g. project_…). Required when using a Personal Access Token; not needed for project-scoped keys. |
TENSORLAKE_API_URL |
https://api.tensorlake.ai |
Override the management API base URL |
TENSORLAKE_IMAGE |
(server default) | Container image to use when creating a new sandbox. Leave unset to use the platform default. |
TENSORLAKE_CPUS |
2 |
Number of vCPUs allocated to the sandbox |
TENSORLAKE_MEMORY_MB |
4096 |
RAM allocated to the sandbox in MB |
TENSORLAKE_DISK_MB |
10240 |
Ephemeral disk size allocated to the sandbox in MB |
TENSORLAKE_SANDBOX_PROXY_URL |
(auto) | Override the sandbox proxy URL (useful for local development). When set, all sandboxes use this single URL instead of the https://{id}.sandbox.tensorlake.ai pattern |
-
Set the environment variable and start OpenCode in a project directory:
export TENSORLAKE_API_KEY=your_key opencode -
After OpenCode loads, the plugin is active but no sandbox exists yet — sandboxes are created on the first intercepted tool call, not at startup. Confirm the plugin loaded by tailing its log:
tail -f ~/.local/share/opencode/log/tensorlake.logOn startup you should see a single line:
[2024-01-15T10:00:01.000Z] [INFO] OpenCode started with TensorLake pluginIf this file doesn't exist after launch, the plugin never loaded — see Plugin not loading.
-
Trigger sandbox creation by asking the model to run something (see the bash test below). The first tool call provisions the sandbox; you'll then see a toast — "Sandbox created - New sandbox is ready." — and new log lines:
[2024-01-15T10:00:30.200Z] [INFO] Creating new sandbox for session abc123 [2024-01-15T10:00:32.500Z] [INFO] Sandbox created sandbox-xyz in 2300ms
In the OpenCode chat prompt, type:
Run: echo "hello from sandbox" && uname -a
The model will call the bash tool. Because the plugin intercepts it, the command executes inside the TensorLake sandbox. You should see Linux kernel information from the sandbox VM rather than your local machine.
Write the text "Hello TensorLake" to /tmp/workspace/test.txt, then read it back.
The model will:
- Call
writewithfilePath=/tmp/workspace/test.txt→ routed tosandbox.writeFile()via SDK - Call
readwithfilePath=/tmp/workspace/test.txt→ routed tosandbox.readFile()via SDK
The response should echo back Hello TensorLake.
List the files in /tmp/workspace
This triggers the ls tool, which calls sandbox.listDirectory('/tmp/workspace') via the SDK.
Delete the OpenCode session from the session list. The plugin handles the session.deleted event and calls sdk.delete(sandboxId) via the TensorLake SDK. Confirm in the log:
[…] [INFO] Deleting sandbox sandbox-xyz for session abc123
[…] [INFO] Sandbox sandbox-xyz deleted
This is expected. The plugin does not create a sandbox at launch — a sandbox is provisioned lazily on the first intercepted tool call (bash, read, write, edit, ls, glob, grep) in a session. Until the model runs a tool, there is no sandbox and no "Sandbox created" toast.
To start one, ask the model to run a command (e.g. Run: uname -a). If you've made a tool call and still see no sandbox, check ~/.local/share/opencode/log/tensorlake.log:
- File is missing entirely → the plugin never loaded. See Plugin not loading.
- File exists but logs an auth error (401/403) → verify
TENSORLAKE_API_KEYis exported in the same environment OpenCode runs in (echo $TENSORLAKE_API_KEY). For PAT keys, also setTENSORLAKE_ORGANIZATION_IDandTENSORLAKE_PROJECT_ID. - File exists and logs
Creating new sandboxbut it hangs or errors → the API call to create the sandbox failed; check the error message in the log.
Note: OpenCode installs the npm package into its own cache (
~/.cache/opencode/packages/), not your project or globalnode_modules. You don't need tonpm installthe plugin yourself — listing it inopencode.jsonis enough. A manualnpm installelsewhere has no effect on what OpenCode loads.
Check the OpenCode server log for errors:
ls -lt ~/.local/share/opencode/log/*.log | head -3
cat ~/.local/share/opencode/log/<latest>.log | grep -i "plugin\|error\|tensorlake""Plugin export is not a function" — The path in opencode.json points to the repository root instead of the .ts entry file. Make sure the path ends with .opencode/plugin/index.ts.
"404 failed to install plugin" — For npm installs, verify the package name is tensorlake-opencode. For local development installs, this usually means the file:// prefix is missing and OpenCode is trying to fetch your local path as an npm package name. Add file:// before the absolute path.
If OpenCode loads but commands run locally (not in the sandbox), the plugin tools are not being registered. Verify by checking the server log for a second round of tool registrations after the built-ins:
service=tool.registry status=started bash ← built-in
...
service=tool.registry status=started bash ← plugin override (should appear)
If the second block is absent, the plugin loaded but failed to return its hooks. Check tensorlake.log for errors logged during startup.
Suspension requires the sandbox to have been named at creation time. Sandboxes created before this was implemented (ephemeral) cannot be suspended. Delete the old session from OpenCode's session list to trigger cleanup, then create a new session — new sandboxes are always named.
opencode-tensorlake-plugin/
├── package.json
├── tsconfig.json # type-check config (no emit)
├── tsconfig.lib.json # build config (emits to dist/)
└── .opencode/
└── plugin/
├── index.ts # re-exports default plugin
└── tensorlake/
├── index.ts # plugin factory
├── tools.ts # assembles all tools
├── core/
│ ├── client.ts # TensorLake SDK client (SandboxClient wrapper)
│ ├── logger.ts # file-based logger with rotation
│ ├── session-manager.ts # sandbox lifecycle management
│ ├── toast.ts # TUI toast queue
│ └── types.ts # shared type definitions
├── tools/
│ ├── bash.ts
│ ├── read.ts
│ ├── write.ts
│ ├── edit.ts
│ ├── ls.ts
│ ├── glob.ts
│ └── grep.ts
└── plugins/
├── custom-tools.ts # wires tools into plugin return value
├── session-events.ts # handles session.deleted event
└── system-transform.ts # injects sandbox context into system prompt
npm run type-checknpm run buildOutput is emitted to dist/ with declaration files. The tsconfig.lib.json sets rootDir to .opencode/plugin so the output mirrors that structure under dist/. The main field in package.json points to dist/index.js.
To change the default CPU/memory/disk allocation, set environment variables before starting OpenCode:
export TENSORLAKE_CPUS=4
export TENSORLAKE_MEMORY_MB=8192
export TENSORLAKE_DISK_MB=20480Or edit the defaults directly in TensorLakeClient.createSandbox inside .opencode/plugin/tensorlake/core/client.ts:
const cpus = parseFloat(process.env.TENSORLAKE_CPUS ?? '2')
const memoryMb = parseInt(process.env.TENSORLAKE_MEMORY_MB ?? '4096', 10)
const ephemeralDiskMb = parseInt(process.env.TENSORLAKE_DISK_MB ?? '10240', 10)Set TENSORLAKE_IMAGE to a registered image name before starting OpenCode:
export TENSORLAKE_IMAGE=my-custom-imageWhen unset, the platform's default image is used. To register a custom image:
tl sbx image create Dockerfile --registered-name my-custom-image- Create a new file in
.opencode/plugin/tensorlake/tools/mytool.tsfollowing the same pattern as the existing tools. - Import and register it in
.opencode/plugin/tensorlake/tools.ts.
Each tool factory receives (sessionManager, projectId, worktree, pluginCtx) and returns an object with description, args (a Zod schema map), and execute(args, ctx).