sha256:baf6b637d37fb2c0cea2c8c2d50392b57302d5b7a3eb6e36dd4308921f585b13
Last pushed
4 days by docker
Type
Sandbox Kit
Manifest digest
sha256:baf6b637d37fb2c0cea2c8c2d50392b57302d5b7a3eb6e36dd4308921f585b13
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, pinned to 2.1.287), plus an organization-managed Claude Code *mod* (this repo''s `claude/mod`, loaded from a root-owned directory marketplace via managed settings) that runs a child shim (this repo''s `claude/src/shim.ts`) exposing the Harness Steering Protocol (hsp/1): the shim serves `$HSP_SOCKET`, and hands each accepted steer back to the mod, which injects it into the conversation (`$.session.append` mid-turn, `$.prompt.submit` when idle). 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 claude/README.md ("Claude Code adapter and sandbox kit: claude-hsp-kit"), for the diff summary; the only changes are this description, `sandbox.image`, `sandbox.entrypoint`, `name`, `displayName`, and the `extraKnownMarketplaces`/`enabledPlugins`/`appendPlugins` keys baked into `/etc/claude-code/managed-settings.json` at image build time (claude/kit/Dockerfile). Note: the mod loads in every session launched from this image, but is a no-op unless the platform set `$HSP_SOCKET` (HSP spec 4.2). Unlike the previous settings-hook design there is no `crossSessionInbound` key any more: nothing writes to Claude Code''s cross-session inbox socket.'
sandbox:
image: docker.io/docker/hsp:claude-base-image-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
- console.anthropic.com
- claude.ai
- mcp-proxy.anthropic.com
- platform.claude.com
- downloads.claude.ai
- claude.com
- code.claude.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
- |
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)