sha256:7417700f7bcd420f57488da41348312a638b223a64c9f975df0f328e4bf4a62a
Last pushed
20 days by simonferquel
Type
Sandbox Kit
Manifest digest
sha256:7417700f7bcd420f57488da41348312a638b223a64c9f975df0f328e4bf4a62a
schemaVersion: "2"
kind: sandbox
name: claude-hsp-kit
displayName: Claude Code (HSP Steering)
description: 'HSP-capable variant of the built-in `claude` sandbox kit -- the same `claude` CLI (stock `--dangerously-skip-permissions` entrypoint, no fork, no research-preview flags), plus a `SessionStart`-hook-launched sidecar shim (this repo''s `claude/`) that exposes the Harness Steering Protocol (hsp/1) by writing accepted steers into Claude Code''s own cross-session inbox Unix socket. Built to test/demo the Agentic Platform''s custom-kit-only HSP capability (APT-1293/APT-1382). Everything below this point through `agentInstructions` is copied verbatim from the built-in `claude` kit spec (docker/sandboxes, sandboxlib/agentkits/agents/claude/spec.yaml) -- see this repo''s root README.md, section 11 ("Claude Code adapter and sandbox kit: claude-hsp-kit"), for the diff summary; the only changes are this description, `sandbox.image`, `sandbox.entrypoint`, and the `hooks`/`crossSessionInbound` keys baked into `/etc/claude-code/managed-settings.json` at image build time (kits/claude-hsp-kit/Dockerfile) -- nothing in this file itself changes beyond `sandbox.image`, `sandbox.entrypoint`, `name`, `displayName`, and this description. Note: `crossSessionInbound: "accept"` applies to every session launched from this image, not only HSP-enabled ones (managed-settings.json is a static, image-baked file -- it cannot be conditioned on the per-launch $HSP_SOCKET environment variable the platform sets only for HSP-capable sessions). This is an accepted, documented trade-off, not an oversight: see README ยง11.1 for why it does not widen this kit''s trust boundary in practice (the inbox socket Claude Code binds is already restricted to the same OS user, inside the same sandbox, where every co-located process already has full filesystem/exec access to the agent''s session).'
sandbox:
image: docker.io/simonferquel/claude-hsp-base:latest
entrypoint:
- claude-hsp
agentInstructions:
filename: CLAUDE.md
content: |
## Claude Code: Environment Persistence
The `CLAUDE_ENV_FILE` environment variable is set to `/etc/sandbox-persistent.sh`.
According to [Claude Code Documentation](https://code.claude.com/docs/en/settings#bash-tool-behavior):
> **CLAUDE_ENV_FILE**
>
> If set, this file will be sourced before each Bash command execution. This allows environment variables to persist across multiple Bash tool invocations.
This means `/etc/sandbox-persistent.sh` is sourced before every Bash tool call you make. Any `export` statements you append to this file will be available in all subsequent commands without needing a login shell.
### Shell Completions Must NOT Be in CLAUDE_ENV_FILE
**NEVER add shell completion scripts to `/etc/sandbox-persistent.sh`.**
`CLAUDE_ENV_FILE` is sourced **before every single bash command execution**, not just during shell initialization. Completion scripts rely on special variables (`COMP_WORDS`, `COMP_CWORD`, `COMPREPLY`) that only exist during tab-completion contexts.
#### WRONG - Will Break Bash
```bash
# DO NOT ADD THESE TO /etc/sandbox-persistent.sh
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
[[ -s "$SDKMAN_DIR/etc/bash_completion.sh" ]] && source "$SDKMAN_DIR/etc/bash_completion.sh"
```
#### CORRECT - Only Load Core Functionality
```bash
# ONLY add the main initialization scripts
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]] && source "$SDKMAN_DIR/bin/sdkman-init.sh"
```
#### Symptoms of Broken Shell
When completion scripts are incorrectly added:
- All bash commands return no output (silent failure)
- `echo`, `pwd`, and other basic commands produce no results
- The bash tool becomes completely unusable
#### Solution
If you accidentally added completion scripts and broke the shell:
1. Remove the completion line(s) from `/etc/sandbox-persistent.sh`
2. Exit and restart the Claude Code session
3. Verify with `echo "test"` that bash works again
permissions:
network:
allow:
- api.anthropic.com
- platform.claude.com
- downloads.claude.ai
- claude.com
- code.claude.com
- mcp-proxy.anthropic.com
- bridge.claudeusercontent.com
volumes:
- path: /home/agent/.claude/projects
size: 2g
- path: /home/agent/.claude/sessions
size: 512m
- path: /home/agent/.claude/todos
size: 512m
- path: /home/agent/.claude/shell-snapshots
size: 512m
- path: /home/agent/.claude/statsig
size: 512m
credentials:
- service: anthropic
apiKey:
name: ANTHROPIC_API_KEY
inject:
- domain: api.anthropic.com
header: x-api-key
format: '%s'
- domain: console.anthropic.com
header: x-api-key
format: '%s'
- domain: claude.ai
header: x-api-key
format: '%s'
- domain: mcp-proxy.anthropic.com
header: x-api-key
format: '%s'
oauth:
tokenEndpoint:
host: platform.claude.com
path: /v1/oauth/token
sentinels:
accessToken: sk-ant-oat01-proxy-managed
refreshToken: sk-ant-ort01-proxy-managed
credentialFile:
path: ~/.claude/.credentials.json
template: '{"claudeAiOauth":{"accessToken":"{{.AccessToken}}","refreshToken":"{{.RefreshToken}}","expiresAt":{{.ExpiresAt}},"scopes":{{.ScopesJSON}}}{{if .PrimaryApiKey}},"primaryApiKey":"{{.PrimaryApiKey}}"{{end}}}'
skipIfEnv:
- ANTHROPIC_API_KEY
environment:
variables:
IS_SANDBOX: "1"
setup:
install:
- command: |
set -e
ws="${WORKSPACE_DIR:-/}"
esc=$(printf '%s' "$ws" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g')
projects="\"/\": { \"hasTrustDialogAccepted\": true }"
[ "$ws" = "/" ] || projects="$projects, \"$esc\": { \"hasTrustDialogAccepted\": true }"
printf '%s\n' "{
\"bypassPermissionsModeAccepted\": true,
\"hasCompletedOnboarding\": true,
\"projects\": { $projects }
}" > /home/agent/.claude.json
chown agent:agent /home/agent/.claude.json
user: "0"
description: Seed Claude bypass/trust flags (root; overwrites image-shipped file)
- command: |
set -e
mkdir -p /home/agent/.claude
HELPER=''
if [ "${SBX_CRED_ANTHROPIC_MODE:-none}" != none ]; then
HELPER=' "apiKeyHelper": "echo proxy-managed",
'
fi
printf '%s' "{
\"themeId\": 1,
\"alwaysThinkingEnabled\": true,
${HELPER} \"permissions\": { \"defaultMode\": \"bypassPermissions\" },
\"bypassPermissionsModeAccepted\": true,
\"skipDangerousModePermissionPrompt\": true
}
" > /home/agent/.claude/settings.json
user: agent
description: Seed Claude settings.json from SBX_CRED_ANTHROPIC_MODE
- command: |
set -e
[ -n "$MCP_GATEWAY_URL" ] || exit 0
export PATH="$HOME/.local/bin:$HOME/.claude/local:$PATH"
claude mcp add mcp-gateway "$MCP_GATEWAY_URL" \
--transport http \
--scope user \
--header "Authorization: Bearer $MCP_SENTINEL_TOKEN_NAME" || true
user: agent
description: Register the sandbox MCP gateway with claude (install-time, race-free)
- command: |
set -e
[ -n "$MCP_GATEWAY_URL" ] || exit 0
node <<'NODE' || true
const fs = require('fs');
const path = require('path');
const home = process.env.HOME || '/home/agent';
const file = path.join(home, '.claude.json');
try {
const cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
const entry = cfg && typeof cfg === 'object' && cfg.mcpServers && typeof cfg.mcpServers === 'object'
? cfg.mcpServers['mcp-gateway']
: undefined;
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
entry.alwaysLoad = true;
const tmp = file + '.mcp-gateway-alwaysload-tmp-' + process.pid;
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n');
fs.renameSync(tmp, file);
}
} catch (err) {
console.error('claude-hsp-kit: could not merge alwaysLoad into ' + file + ': ' + err.message);
}
NODE
user: agent
description: Merge alwaysLoad:true into the mcp-gateway MCP server entry (install-time, always-load/direct-exposure)
startup:
- command:
- sh
- -c
- chown -R agent:agent /home/agent/.claude/projects /home/agent/.claude/sessions /home/agent/.claude/todos /home/agent/.claude/shell-snapshots /home/agent/.claude/statsig 2>/dev/null || true
user: "0"
description: Re-own claude session-state volume mount roots to agent (block-volume mkfs trap)
- command:
- sh
- -c
- command -v apt-get > /dev/null 2>&1 && (apt-get update -qq -y > /dev/null 2>&1 || true) &
user: root
description: Update apt package cache in background
- command:
- sh
- -c
- |
set -e
[ -n "$MCP_GATEWAY_URL" ] || exit 0
export PATH="$HOME/.local/bin:$HOME/.claude/local:$PATH"
claude mcp add mcp-gateway "$MCP_GATEWAY_URL" \
--transport http \
--scope user \
--header "Authorization: Bearer $MCP_SENTINEL_TOKEN_NAME" || true
user: agent
description: Register the sandbox MCP gateway with claude (startup fallback)
- command:
- sh
- -c
- |
set -e
[ -n "$MCP_GATEWAY_URL" ] || exit 0
node <<'NODE' || true
const fs = require('fs');
const path = require('path');
const home = process.env.HOME || '/home/agent';
const file = path.join(home, '.claude.json');
try {
const cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
const entry = cfg && typeof cfg === 'object' && cfg.mcpServers && typeof cfg.mcpServers === 'object'
? cfg.mcpServers['mcp-gateway']
: undefined;
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
entry.alwaysLoad = true;
const tmp = file + '.mcp-gateway-alwaysload-tmp-' + process.pid;
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n');
fs.renameSync(tmp, file);
}
} catch (err) {
console.error('claude-hsp-kit: could not merge alwaysLoad into ' + file + ': ' + err.message);
}
NODE
user: agent
description: Merge alwaysLoad:true into the mcp-gateway MCP server entry (startup fallback, always-load/direct-exposure)