Problem
get-claude-key.sh is baked into the Docker image at /usr/local/bin/get-claude-key.sh (via COPY in containers/agent/Dockerfile:83), but is not accessible inside the chroot because the chroot bind-mounts the host's /usr over the container's /usr.
How it fails
Dockerfile copies get-claude-key.sh → /usr/local/bin/get-claude-key.sh (inside the image) ✅
docker-manager.ts:1455 sets CLAUDE_CODE_API_KEY_HELPER=/usr/local/bin/get-claude-key.sh ✅
entrypoint.sh:149-221 writes apiKeyHelper config to .claude.json and .claude/settings.json pointing to /usr/local/bin/get-claude-key.sh ✅
- Chroot mode:
docker-manager.ts:768 bind-mounts /usr:/host/usr:ro — the host's /usr replaces the container's /usr in the chroot filesystem
- Inside chroot,
/usr/local/bin/get-claude-key.sh → host's /usr/local/bin/get-claude-key.sh → does not exist ❌
- Claude Code finds the
apiKeyHelper config but the script fails with exit 127 (not found)
- All API calls fail with
EHOSTUNREACH — zero tokens consumed
apiKeyHelper failed: exited 127: /bin/sh: 1: /usr/local/bin/get-claude-key.sh: not found
Why one-shot-token.so works but get-claude-key.sh doesn't
one-shot-token.so has the exact same problem but was already fixed (entrypoint.sh:406-427): it's explicitly copied from the container's /usr/local/lib/one-shot-token.so to /host/tmp/awf-lib/one-shot-token.so before the chroot happens. The chroot then sees it at /tmp/awf-lib/one-shot-token.so.
get-claude-key.sh has no equivalent copy step.
Root Cause
The chroot overlay means any file baked into the Docker image under /usr/ is shadowed by the host's /usr/ bind mount. Files that need to be accessible inside the chroot must be explicitly copied to a writable path (like /tmp/) before chroot activation.
Proposed Fix
Follow the one-shot-token.so pattern — copy get-claude-key.sh to /host/tmp/awf-lib/ before chroot, and update the apiKeyHelper config path accordingly.
1. In containers/agent/entrypoint.sh (chroot section, near line 406)
Add a copy step for get-claude-key.sh:
# Copy get-claude-key.sh to chroot-accessible path
# The script is in the Docker image at /usr/local/bin/ but the chroot
# bind-mounts the host's /usr, shadowing the container's copy.
if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ] && [ -f "$CLAUDE_CODE_API_KEY_HELPER" ]; then
if mkdir -p /host/tmp/awf-lib 2>/dev/null; then
CHROOT_KEY_HELPER="/tmp/awf-lib/get-claude-key.sh"
if cp "$CLAUDE_CODE_API_KEY_HELPER" "/host${CHROOT_KEY_HELPER}" 2>/dev/null && \
chmod +x "/host${CHROOT_KEY_HELPER}" 2>/dev/null; then
CLAUDE_CODE_API_KEY_HELPER="$CHROOT_KEY_HELPER"
echo "[entrypoint] Claude key helper copied to chroot at ${CHROOT_KEY_HELPER}"
else
echo "[entrypoint][WARN] Could not copy get-claude-key.sh to chroot"
fi
fi
fi
2. Update apiKeyHelper config to use the chroot-accessible path
The apiKeyHelper config writing (lines 149-221) must use the updated path. This means the copy step should run before the config writing, or the config writing should check for the chroot-corrected path.
3. Consider a general pattern
Any script baked into the image under /usr/ that needs chroot access should use this pattern. A helper function could centralize this:
copy_to_chroot() {
local src="$1" dst_name="$2"
local dst="/tmp/awf-lib/${dst_name}"
if [ -f "$src" ]; then
mkdir -p /host/tmp/awf-lib 2>/dev/null
if cp "$src" "/host${dst}" 2>/dev/null && chmod +x "/host${dst}" 2>/dev/null; then
echo "$dst"
return 0
fi
fi
return 1
}
Affected Versions
- AWF v0.25.1 through v0.25.4 (chroot mode with Claude engine)
- Non-chroot mode is unaffected (the container's
/usr/local/bin/ is not shadowed)
References
Problem
get-claude-key.shis baked into the Docker image at/usr/local/bin/get-claude-key.sh(viaCOPYincontainers/agent/Dockerfile:83), but is not accessible inside the chroot because the chroot bind-mounts the host's/usrover the container's/usr.How it fails
Dockerfilecopiesget-claude-key.sh→/usr/local/bin/get-claude-key.sh(inside the image) ✅docker-manager.ts:1455setsCLAUDE_CODE_API_KEY_HELPER=/usr/local/bin/get-claude-key.sh✅entrypoint.sh:149-221writesapiKeyHelperconfig to.claude.jsonand.claude/settings.jsonpointing to/usr/local/bin/get-claude-key.sh✅docker-manager.ts:768bind-mounts/usr:/host/usr:ro— the host's/usrreplaces the container's/usrin the chroot filesystem/usr/local/bin/get-claude-key.sh→ host's/usr/local/bin/get-claude-key.sh→ does not exist ❌apiKeyHelperconfig but the script fails with exit 127 (not found)EHOSTUNREACH— zero tokens consumedWhy
one-shot-token.soworks butget-claude-key.shdoesn'tone-shot-token.sohas the exact same problem but was already fixed (entrypoint.sh:406-427): it's explicitly copied from the container's/usr/local/lib/one-shot-token.soto/host/tmp/awf-lib/one-shot-token.sobefore the chroot happens. The chroot then sees it at/tmp/awf-lib/one-shot-token.so.get-claude-key.shhas no equivalent copy step.Root Cause
The chroot overlay means any file baked into the Docker image under
/usr/is shadowed by the host's/usr/bind mount. Files that need to be accessible inside the chroot must be explicitly copied to a writable path (like/tmp/) before chroot activation.Proposed Fix
Follow the
one-shot-token.sopattern — copyget-claude-key.shto/host/tmp/awf-lib/before chroot, and update theapiKeyHelperconfig path accordingly.1. In
containers/agent/entrypoint.sh(chroot section, near line 406)Add a copy step for
get-claude-key.sh:2. Update
apiKeyHelperconfig to use the chroot-accessible pathThe
apiKeyHelperconfig writing (lines 149-221) must use the updated path. This means the copy step should run before the config writing, or the config writing should check for the chroot-corrected path.3. Consider a general pattern
Any script baked into the image under
/usr/that needs chroot access should use this pattern. A helper function could centralize this:Affected Versions
/usr/local/bin/is not shadowed)References
get-claude-key.shsource:containers/agent/get-claude-key.shcontainers/agent/Dockerfile:83src/docker-manager.ts:1455containers/agent/entrypoint.sh:149-221one-shot-token.socopy pattern:containers/agent/entrypoint.sh:406-427/usrbind mount:src/docker-manager.ts:768