Sign inSign up

simonferquel/claude-hsp-kit:latest

Manifest digest

sha256:7417700f7bcd420f57488da41348312a638b223a64c9f975df0f328e4bf4a62a

Last pushed

20 days by simonferquel

Type

Sandbox Kit

Manifest digest

sha256:7417700f7bcd420f57488da41348312a638b223a64c9f975df0f328e4bf4a62a

yaml
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)