Skip to content

Getting started with Box

Box runs an agent on Linux or macOS under a policy. The agent can reach only the files, programs, and network destinations you allow. You describe a box in two files: box.toml says what to run and what it can reach, and policy.dw decides each request it makes.

In this guide you’ll put the Strands CLI in a box on macOS. You’ll give it one tool, a shell, so your policy decides every command it runs and every file its shell reads or writes, and you’ll watch the box refuse a write, then change the policy to allow it. The finished files are in examples/strands-cli.

Paste this prompt into your coding agent to follow this page:

Show the prompt
Help me put the Strands CLI in a Strands Box on this Mac. Fetch this guide and follow it step by
step, and prefer it over what you remember about Box:
https://raw.githubusercontent.com/strands-agents/box/f4180a035d2d164596135864604629529636bf1c/docs/user/getting-started.md
Rules:
- First, check the guide's "Before you start" list. Tell me what's missing, and stop if something
blocks the guide.
- Ask me before each step that installs software or writes files. Use ~/box-tutorial, as the
guide does.
- Don't create, read, or store my Bedrock API key. Tell me how to make one, and give me the export
command to run in my own terminal. Don't ask me to paste the key to you.
- Don't run `box run` yourself, because it starts an interactive agent. Give me the exact command
to run in the terminal where I exported the key.
- If the box fails, ask me for the error, and use the guide's "If something goes wrong" table and
its decision log command to find the cause.
- When the box works, offer to help me change what it allows in my-box/policy.dw. The
policy-authoring skill at
https://raw.githubusercontent.com/strands-agents/box/f4180a035d2d164596135864604629529636bf1c/.agents/skills/authoring-box-policy/SKILL.md
teaches the Dogwood policy language and the box's action vocabulary; fetch it and follow it.

Box supports macOS on Apple silicon and Linux ARM64 (64-bit Arm). On Linux, use Amazon Linux 2023 ARM64 with glibc 2.34 or newer and libgcc. Step 1 installs Box on either platform. The Strands CLI walkthrough in steps 2–8 uses macOS. For that walkthrough, you need:

  • A Mac with Apple silicon and macOS 15 or later.
  • Node.js 22.21 or later from Homebrew: brew install node.
  • An AWS account with access to Claude Opus 5 on Amazon Bedrock in us-west-2.

The files in this guide use the directory ~/box-tutorial. Make it, and run every command from there:

Terminal window
mkdir ~/box-tutorial
cd ~/box-tutorial

Download Box on macOS or Linux:

Terminal window
curl -fsSL https://raw.githubusercontent.com/strands-agents/box/refs/heads/main/download.sh | sh
./box-core/box --version

The script downloads the latest release for your operating system, verifies its checksum, and checks the extracted files in temporary storage. It then unpacks the archive into ./box-core. Run box by its path, and keep the entire directory together. The version command prints strands-box followed by the release version.

Set BOX_VERSION to select a release tag and BOX_DOWNLOAD_DIR to select a destination. Use a fresh destination to keep a previous installation if the final extraction fails.

The signed macOS package contains Box.app, with a box launcher and helper links into that bundle. Moving only an executable out of the bundle loses its provisioning profile. The package is also notarized, so macOS runs a copy that you download in a browser.

After installation, continue with the Amazon EKS coding example or the AgentCore Runtime shipment example. Steps 2–8 below follow the macOS Strands CLI example.

Check Linux setup requirements for the permissions Box needs at startup. Use the platform comparison when adapting a macOS configuration.

Build from source (optional)

To build Box yourself, install rustup. Build it and copy the binaries into ./box-core:

Terminal window
git clone https://github.com/strands-agents/box.git box-src
cargo build --release --manifest-path box-src/Cargo.toml -p strands-box -p strands-box-containment
mkdir box-core
cp box-src/target/release/strands-box box-core/box
cp box-src/target/release/strands-box-sock-alias box-src/target/release/strands-box-contain-trampoline box-core/

Install the CLI with Homebrew’s npm, so it lands where box.toml expects it, and make a small project for it to work on:

Terminal window
/opt/homebrew/bin/npm install -g @strands-agents/cli
mkdir my-project
echo "# My project" > my-project/README.md

Send the AWS SDK through the egress gateway

Section titled “Send the AWS SDK through the egress gateway”

Note. Not every agent runs in a box unchanged. The box sends all of the agent’s network traffic through its egress gateway, and sets HTTPS_PROXY to the gateway’s address. The AWS SDK for JavaScript opens its own connections and ignores HTTPS_PROXY, and its Bedrock calls use HTTP/2, which no proxy setting reaches. That’s a general Node.js problem: any Node program that calls AWS from behind a proxy has it. The Strands CLI needs a few more changes in the steps below. Without each one, the box fails at startup or refuses a request, and stderr or the decision log says why.

Node can load a file before the CLI starts, and that file can send the SDK’s requests through the gateway. Save this file as proxy-preload.mjs:

// Send the AWS SDK's requests through the box's egress gateway. Load with `node --import`.
import { createRequire } from "node:module";
// Find modules the way the CLI does, from the CLI's own script.
const require = createRequire(process.argv[1]);
if (process.env.HTTPS_PROXY) {
// Make every Node connection use the proxy settings from the environment.
const http = require("node:http");
const https = require("node:https");
const proxyEnv = process.env;
const HttpAgent = http.Agent;
const HttpsAgent = https.Agent;
http.Agent = class extends HttpAgent {
constructor(options) {
super({ proxyEnv, ...options });
}
};
https.Agent = class extends HttpsAgent {
constructor(options) {
super({ proxyEnv, ...options });
}
};
// Bedrock calls use HTTP/2, which ignores these proxy settings. Use HTTP/1.1.
const handlers = require("@smithy/node-http-handler");
handlers.NodeHttp2Handler = handlers.NodeHttpHandler;
}

The CLI reads its settings from ~/.strands/cli/config.json; the next step will set the CLI’s HOME to ./strands-home, so it reads them from there, but first we need to create a box-specific settings file.

Make the directory:

Terminal window
mkdir -p strands-home/.strands/cli

Then save this file as strands-home/.strands/cli/config.json:

{
"onboarding": { "version": 1 },
"providers": { "enabled": ["bedrock"] },
"profile": { "model": "bedrock/global.anthropic.claude-opus-5" },
"permissions": { "mode": "bypassPermissions" },
"settings": { "setupOnLaunch": false }
}

With these settings the CLI uses Claude Opus 5 on Bedrock, opens straight into a chat, and skips its setup screens. It also no longer asks you to approve each tool call, so the box’s policy decides each command.

Make a directory for the box:

Terminal window
mkdir my-box

Save this file as my-box/box.toml:

# To author or change the policy next to this file, point your coding agent at the skill:
# https://raw.githubusercontent.com/strands-agents/box/f4180a035d2d164596135864604629529636bf1c/.agents/skills/authoring-box-policy/SKILL.md
# The box's name, and where it keeps its own state. The agent can't reach box_dir.
name = "strands"
box_dir = "<HOME>/box-tutorial/my-box/state"
# The policy file, next to this one.
policy = "policy.dw"
[agent]
# The program the box starts. Node loads the preload file first, then runs the CLI.
command = [
"/opt/homebrew/bin/node",
"--import=<HOME>/box-tutorial/proxy-preload.mjs",
"/opt/homebrew/lib/node_modules/@strands-agents/cli/bin/strands.js",
# Give the agent its shell and web fetch, and no file tools, so the policy decides each file it works on.
"--set", 'builtinTools={"*":false,"shell":true,"web_fetch":{"transport":"direct"}}',
# Keep sessions and memory out of the project.
"--set", 'session.dir="<HOME>/box-tutorial/strands-home/sessions"',
"--set", 'memory.dir="<HOME>/box-tutorial/strands-home/memory"',
]
# The directory the agent starts in. This alone grants nothing.
workspace = "<HOME>/box-tutorial/my-project"
# The agent gets these variables, plus the ones the box adds. Nothing comes from your shell.
env = { AWS_REGION = "us-west-2", HOME = "<HOME>/box-tutorial/strands-home", PATH = "/usr/bin:/bin", TERM = "xterm-256color" }
# What the agent's own process can touch without asking the policy.
[agent.filesystem]
# Node loads the CLI, and the CLI reads its settings.
read = ["/opt/homebrew/lib/node_modules/@strands-agents/cli", "~/box-tutorial/strands-home"]
# Node loads the preload file, and reads Homebrew's OpenSSL settings when it starts.
read_file = ["~/box-tutorial/proxy-preload.mjs", "/opt/homebrew/etc/openssl@3/openssl.cnf"]
# The CLI writes its sessions and memory.
write = ["~/box-tutorial/strands-home"]
# The CLI lists the names in its working directory. It can't open the files itself.
list = ["~/box-tutorial/my-project"]
# The box adds your Bedrock API key to each request to Bedrock. The agent, and every tool or MCP
# server in the box, gets a stand-in value.
# "model" is a name you choose.
[egress.model]
destinations = ["bedrock-runtime.us-west-2.amazonaws.com"]
secret.ref = "env://AWS_BEARER_TOKEN_BEDROCK"

The paths in box_dir, command, workspace, and env must be absolute. The filesystem lists can start with ~, which the box expands, so only the <HOME> paths need your home directory:

Terminal window
sed -i '' "s|<HOME>|$HOME|g" my-box/box.toml

Then save this file as my-box/policy.dw. It allows calls to Bedrock, any shell command, reads in the project, and /dev/null:

// Connect to Bedrock, and send it requests.
@id("model_connect")
permit (principal, action == Box::Action::"net:connect", resource)
when { context.input.host == "bedrock-runtime.us-west-2.amazonaws.com" && context.input.port == 443 };
@id("model_request")
permit (principal, action == Box::Action::"http:request", resource)
when { context.input.host == "bedrock-runtime.us-west-2.amazonaws.com" };
// Run any command in the box's shell. Each file a command touches is still its own decision.
@id("shell_commands")
permit (principal, action == Box::Action::"shell:exec", resource);
// Read anything in the project. A path under your home starts with "~" here.
@id("project_read")
permit (principal, action == Box::Action::"fs:read", resource)
when {
context.input.path == "~/box-tutorial/my-project" ||
context.input.path like "~/box-tutorial/my-project/*"
};
// Use /dev/null, which agents redirect output to all the time.
@id("dev_null")
permit (principal, action in [Box::Action::"fs:read", Box::Action::"fs:write"], resource)
when { context.input.path == "/dev/null" };

The box refuses any request that no permit matches.

In the Amazon Bedrock console, pick the us-west-2 region, open API keys, and on the Short-term API keys tab choose Generate short-term API keys. Put the key in your shell:

Terminal window
export AWS_BEARER_TOKEN_BEDROCK="your-key"

To make a key from your AWS credentials instead, run this. Replace your-profile with your AWS profile:

Terminal window
/opt/homebrew/bin/npm install --prefix ./bedrock-key @aws/bedrock-token-generator
export AWS_BEARER_TOKEN_BEDROCK="$(AWS_PROFILE=your-profile AWS_REGION=us-west-2 /opt/homebrew/bin/node -e 'require("./bedrock-key/node_modules/@aws/bedrock-token-generator").getTokenProvider()().then(console.log)')"

A key lasts up to 12 hours, and works only in the region you made it in. Run the box from the same shell.

Terminal window
./box-core/box run --config my-box/box.toml

The box prints what the agent can touch, then the CLI opens a chat. The start of that output looks like this:

strands-box: box box-4a4cdb2b114ab6ce created · config my-box/box.toml
strands-box: starting workload
strands-box: [agent] runs /opt/homebrew/Cellar/node/26.5.0/bin/node with no policy decision over these paths:
read /opt/homebrew/lib/node_modules/@strands-agents/cli
read /Users/you/box-tutorial/strands-home
write /Users/you/box-tutorial/strands-home
read_file /Users/you/box-tutorial/proxy-preload.mjs
read_file /opt/homebrew/etc/openssl@3/openssl.cnf
list /Users/you/box-tutorial/my-project

Ask the agent:

Read README.md, then add a line to it that says hello.

The agent reads README.md, and the box refuses the write, because the policy allows only reads. The agent tells you the write was denied. Type /exit to leave the chat, which stops the box.

The box writes every decision to my-box/state/private/telemetry/records.jsonl. This prints one line per decision:

Terminal window
jq -r '.resourceLogs[]?.scopeLogs[].logRecords[]
| (.attributes | map({(.key): .value}) | add) as $a
| select($a["strands.box.policy.verdict"])
| [$a["strands.box.policy.verdict"].stringValue, $a["strands.box.policy.action"].stringValue,
($a["file.path"].stringValue // $a["server.address"].stringValue
// ($a["process.command_args"].arrayValue.values | map(.stringValue) | join(" ")))]
| @tsv' my-box/state/private/telemetry/records.jsonl

Example output:

deny net:connect telemetry.strandsagents.com
deny shell:spawn /usr/bin/uname -s
permit shell:exec pwd
permit net:connect bedrock-runtime.us-west-2.amazonaws.com
permit http:request bedrock-runtime.us-west-2.amazonaws.com
permit shell:exec cat /Users/you/box-tutorial/my-project/README.md
permit fs:read ~/box-tutorial/my-project/README.md
permit net:connect bedrock-runtime.us-west-2.amazonaws.com
permit http:request bedrock-runtime.us-west-2.amazonaws.com
permit shell:exec cd /Users/you/box-tutorial/my-project
permit fs:read ~/box-tutorial/my-project
permit shell:exec printf hello\\n
deny fs:write ~/box-tutorial/my-project/README.md

Notice that each model call results in a net:connect and an http:request, and each command results in a shell:exec, plus an fs: decision for every file it touches.

The box’s shell has cat, printf, ls, and about thirty other commands built in. Running any other program is a shell:spawn, and this policy allows none, so the box refused the uname that the CLI runs when it starts. The box also refused the CLI’s own version ping, because the policy names no host but Bedrock. Nothing you typed asked for either one, which is the point: the log holds what the agent did on its own as well as what you asked for.

Now you can allow writes in the project by adding this permit to the end of my-box/policy.dw:

// Write anything in the project.
@id("project_write")
permit (principal, action == Box::Action::"fs:write", resource)
when { context.input.path like "~/box-tutorial/my-project/*" };

Run the box again and ask the same thing. The box reads policy.dw each time it starts, so the new permit applies and the write goes through this time. The decision log shows it:

permit fs:write ~/box-tutorial/my-project/README.md

That’s the loop with any agent: run it, read what the box refused, and allow only what it needs.

The box writes its own errors to stderr. To keep them, add 2> my-box/box-run.log to the box run command. These are the errors you’re most likely to see, and what fixes each one:

ErrorFix
getaddrinfo ENOTFOUND bedrock-runtime.us-west-2.amazonaws.comNode didn’t load the preload file. Check the --import path in command.
Authentication failed: Please make sure your API Key is valid.Your key has expired, or you made it in another region. Make a new one in us-west-2.
OpenSSL configuration error … openssl.cnfAdd openssl.cnf to read_file.
EPERM: process.cwd failed with error operation not permitted, uv_cwdAdd the project to list.
Unexpected token 'b', "blocked by"... is not valid JSONThe box refused a model request. Find its deny in the decision log, and read the reason.
unsafe box directory ...: it is not empty and contains no valid private recordYou removed part of my-box/state. Remove all of it; run recreates it. Never remove state/private alone.

Stop the box, then delete the directory:

Terminal window
rm -rf ~/box-tutorial