Known Failure Modes
September 9, 2026 · View on GitHub
This document captures real-world failure patterns when using Docker SBX + USAi + agent frameworks. If you're hitting something weird, it's probably in here.
1. "Unknown Agent" Error on sbx create
Symptoms
sbx create my-sandboxfails- Error:
unknown agent "my-sandbox"
Root Cause
SBX create command requires an agent type (e.g., opencode, claude, shell) and a workspace path.
Fix
Use the correct syntax:
# Correct: specify agent type and path
sbx create opencode .
# With custom name
sbx create --name my-sandbox opencode .
Available agents: claude, codex, copilot, docker-agent, gemini, kiro, opencode, shell
2. API Key Works in UI but Fails in Agent
Symptoms
- Works in Swagger / web UI
- Fails in agent or CLI
- Error:
authentication failedor401 Unauthorized
Likely Causes
- Incorrect header format
- Missing
Bearerprefix - Wrong environment variable injection
Fix
Ensure header format is correct:
Authorization: Bearer <API_KEY>
Confirm the injected env var is present inside the sandbox:
acq exec <sandbox> -- sh -c 'echo $USAI_API_KEY'
3. Agent Cannot See API Key / USAi Authentication Fails
Symptoms
- Agent fails silently or errors on auth
- OpenCode shows generic providers instead of USAi
{"detail":"Authentication failed"}from USAi API
Root Cause
SBX's secret proxy only works with standard provider endpoints. USAi uses a custom baseURL (https://api.gsa.usai.gov/api/v1), which the proxy doesn't recognize.
When you use sbx secret set -g openai, SBX:
- Stores your key
- Sets
OPENAI_API_KEY=proxy-managedin the container - Intercepts requests to
api.openai.comand injects the real key
But requests to api.gsa.usai.gov bypass this proxy entirely.
Fix
Store the key as a custom secret so sbx injects it for you:
sbx secret set-custom -g --host api.gsa.usai.gov --env USAI_API_KEY
Recreate the sandbox so the secret takes effect. opencode.jsonc reads the
injected value via {env:USAI_API_KEY}.
4. Secrets Accidentally Printed
Symptoms
- API key appears in logs/output
- Key visible in terminal history
Root Cause
- Debugging via
printenvorenv - Logging config objects that contain credentials
- Shell history capturing secret values
Fix
- Never print full environment
- Mask values if debugging required
- Use
set +o historybefore working with secrets - Review agent logs before sharing
5. Incorrect baseURL
Symptoms
- Model list fails
- 404 or unexpected API errors
- Connection refused
Root Cause
Wrong endpoint format in configuration.
Fix
Use the correct format:
https://api.gsa.usai.gov/api/v1
NOT:
- Missing
/api/v1suffix - Swagger UI URL
- Documentation endpoint
- Trailing slash issues
6. SBX CLI Behavior Changes
Symptoms
- Commands stop working between runs
- Flags behave differently than expected
- Documentation doesn't match actual behavior
Root Cause
SBX tooling is rapidly evolving with frequent breaking changes.
Fix
- Check
sbx --helpfor current syntax - Check
sbx <command> --helpfor subcommand options - Revalidate commands before assuming code failure
- Avoid scripting around unstable flags
- Pin to specific SBX versions if possible
7. Agent Tries to Escape Sandbox
Symptoms
- Attempts to access host filesystem paths
- Unexpected file path errors
- Permission denied on paths that "should" exist
Root Cause
Agent assumes host filesystem layout, not container layout.
Fix
- Enforce working directory constraints in AGENTS.md
- Avoid granting unnecessary volume mounts
- Configure agent with container-relative paths
- Review agent file access patterns
8. Model Appears Available but Fails at Runtime
Symptoms
/modelsendpoint lists the model- Inference requests fail with errors
- "Model not found" despite being listed
Root Cause
- Model not actually enabled for your API key
- Backend routing mismatch
- Model requires specific parameters not provided
Fix
- Test with minimal request first
- Confirm model entitlement with USAi provider
- Check if model requires specific
max_tokensor other params - Try a different model to isolate the issue
9. Long Timeouts / Hanging Requests
Symptoms
- Requests never return
- Agent appears stuck
- Eventually fails with timeout
Root Cause
- Missing timeout configuration
- Network routing issues inside container
- DNS resolution failures
- Proxy misconfiguration
Fix
Set explicit timeouts in config:
{
"requestTimeout": 30000,
"chunkTimeout": 5000
}
Check network connectivity from inside container:
acq exec <sandbox> -- curl -I https://api.gsa.usai.gov/api/v1/models
10. Overcomplicated Setup
Symptoms
- Too many scripts to run
- Hard to reproduce environment
- Works on one machine, fails on another
- Debugging requires tribal knowledge
Root Cause
Over-engineering instead of testing. Adding layers when simplicity would work.
Fix
- Delete unnecessary wrapper scripts
- Prefer 1 config file + 1 command
- Document the minimal reproduction steps
- If setup takes more than 3 commands, simplify
11. False Sense of Security
Symptoms
- Assuming "it's in a container so it's safe"
- Relaxing secret handling because of SBX
- Not reviewing agent outputs
Reality
Containers are NOT perfect isolation:
- Container escapes exist
- Mounted volumes expose host data
- Network access can leak information
- Logs may be captured outside container
Fix
- Treat SBX as a strong boundary, not absolute
- Continue to avoid exposing secrets at all costs
- Review agent outputs before sharing
- Don't mount sensitive host directories
- Apply defense in depth
12. Environment Variable Naming Conflicts
Symptoms
- Agent uses wrong API key
- Configuration seems ignored
- Unexpected behavior with correct config
Root Cause
Multiple tools expecting different env var names:
OPENAI_API_KEYUSAI_API_KEYAPI_KEY- Tool-specific variations
Fix
- Check tool documentation for expected variable names
- Set all expected variations if needed
- Use explicit config file settings over env vars when possible
13. Config File Not Found in Container
Symptoms
- Agent starts with defaults
- Custom configuration ignored
- "Config file not found" warnings
Root Cause
Config file exists on host but not mounted into container.
Fix
Ensure config is in the mounted working directory. When using acq run (or
acq create), the current directory is automatically mounted:
# Run from the directory containing your config
cd /path/to/project-with-config
acq run opencode .
Or copy config into an existing container:
acq cp ./opencode.jsonc my-sandbox:/workspace/
14. SBX Built-in Proxy Doesn't Auto-Cover USAi's Custom baseURL (Use set-custom)
Symptoms
sbx secret set -g openaisucceeds- But USAi authentication still fails
OPENAI_API_KEY=proxy-managedin container- Must inject the key via a custom secret instead
Root Cause
SBX's default provider proxy intercepts requests to a fixed set of known
provider endpoints (like api.openai.com) and injects credentials there. A
custom baseURL endpoint like USAi (api.gsa.usai.gov) is not one of those
built-in services, so sbx secret set -g openai never applies to it — you must
register USAi as a custom secret (sbx secret set-custom) instead. acq
does exactly this for you (acq secret set usai, routed through the
secret set-custom branch of acq.backends/sbx.sh).
Security Implication
Under the sanctioned acq setup, the agent does NOT hold the raw USAi key.
set-custom is proxied the same way the built-in services are — it just has to
be configured explicitly because USAi is not a built-in endpoint.
Concretely, sbx secret set-custom bakes a placeholder token into the
sandbox's USAI_API_KEY at creation time, and the sbx proxy resolves that
placeholder to the real secret value at request time, on the wire to
api.gsa.usai.gov. So:
- Agent sees:
USAI_API_KEY=<placeholder>(not the real key value) - The real credential lives in the sbx secret store on the host, never in the container environment
- The agent process cannot read the raw key from its own environment
This matches the built-in proxy posture (and the MSB --secret ENV@HOST
swap-on-the-wire model), and it is what AGENTS.md states: secrets are
injected placeholders or proxied — the agent never holds the real USAi key
material. (The placeholder mechanism is described in more detail in
Section 23.)
Narrow caveat: non-default paths that DO expose the value
The raw value only reaches the container if you deliberately bypass the sanctioned flow:
- Passing
sbx secret set-custom … --value <the-secret>on the command line puts the value in argv (and shell history).acqnever does this — it prompts interactively or stores the value in the acq store and prints the manual command as a last resort; it does not echo the value on argv. - A hand-rolled setup that exports the raw key into the container env (e.g.
export USAI_API_KEY=<real-key>in a profile, or--valuein a script) defeats the placeholder mechanism.
These are non-default, unsupported configurations. Under acq run the agent
sees a placeholder, not the key.
Mitigations
- Placeholder injection - the agent's env holds a placeholder, not the key
- AGENTS.md rules prohibit printing/logging secrets
- Container isolation limits exposure scope
- No persistence - key never written to disk in the sandbox
Fix
Register USAi as a custom secret so sbx injects the placeholder (prefer acq,
which does this for you):
# Preferred: acq configures the custom secret for both backends
acq secret set usai
# Equivalent raw sbx command (interactive prompt for the value — no --value):
sbx secret set-custom --host api.gsa.usai.gov --env USAI_API_KEY
Your opencode.jsonc should use variable substitution:
{
"provider": {
"usai": {
"options": {
"apiKey": "{env:USAI_API_KEY}"
}
}
}
}
Upstream Tracking
A convenience request to let the proxy inject credentials for custom endpoints
without an explicit set-custom step is tracked in
docker/sbx-releases#35 -
"Feature Request: Configurable Secret Injection for Custom Services". Note this
is an ergonomics improvement (auto-mapping custom services), not a security
gap: set-custom already proxies the credential today, so the agent does not see
the raw key under the current setup.
15. Direct Credential Injection for Git Providers (Security Consideration)
Context
GitHub is a built-in SBX service (sbx secret set -g github), but GitLab is not. This means:
- GitHub: Uses the proxy (recommended)
- GitLab: Must use a custom secret (
sbx secret set-custom -g --host <host> --env GITLAB_TOKEN)
Security Assessment for MVP
| Concern | Severity | Mitigation |
|---|---|---|
| Token visible in container env | Low | Container is isolated, short-lived |
| Token in shell history | Low | sbx prompts for the value; never typed on the command line |
| Token in process list | Low | Stored secret injected by sbx, not passed via process args |
| Agent could exfiltrate token | Medium | Agent already has network access; proxy doesn't prevent this |
| Token logged by agent | Medium | AGENTS.md prohibits; pre-commit hooks catch committed secrets |
Key Insight
The SBX proxy doesn't prevent a malicious agent from exfiltrating credentials - it prevents the agent from seeing them directly. A compromised agent could still make authenticated API calls and exfiltrate data through those APIs.
The real security boundary is:
- Sandbox isolation - container can't escape to host
- Trusted agent software - OpenCode, Claude Code, etc. are vetted
- Scoped tokens - use minimal scopes (e.g.,
repo, not admin) - Short-lived sessions - tokens only in memory during execution
Acceptable for MVP Because
- Local development environment with low-impact data (no PII, no CUI)
- Tokens are scoped - not admin/owner tokens
- Sandbox provides isolation from host system
- Direct injection is a documented SBX pattern - shown in their own docs
- Upstream tracking exists - this is a known gap, not a workaround hack
Recommendations
- Use SBX proxy when available - GitHub supports it, use
sbx secret set -g github - Scope tokens minimally - only grant permissions the agent actually needs
- Rotate tokens periodically - treat injected tokens as potentially exposed
- Review agent outputs - before sharing logs, ensure no tokens leaked
- Monitor API usage - watch for unexpected patterns
If GitHub auth in an existing sandbox starts failing after token rotation, force-refresh the stored global GitHub secret from the host:
gh auth token | sbx secret set -g github --force
Upstream Tracking
- SBX custom service support: docker/sbx-releases#35
- Helper script exploration: GSA-TTS/agentic-coding-quickstart#15
Related
See also: Section 14 - SBX Built-in Proxy Doesn't Auto-Cover USAi's Custom baseURL
16. SSL/TLS Certificate Error: "unable to get local issuer certificate"
Symptoms
- OpenCode crashes or fails to connect to USAi
- Error:
unable to get local issuer certificate - Occurs on GFE (Government Furnished Equipment) networks
Root Cause
On federal/GFE networks, secure internet traffic is often intercepted and decrypted using a custom Root Certificate Authority (CA) for security inspection.
Because the sandboxed container runs a vanilla Linux environment, it does not automatically trust your host's GFE custom root certificate. When Node.js tries to establish a TLS connection to https://api.gsa.usai.gov/api/v1, it rejects the connection because it cannot verify the custom certificate chain.
Fix
To resolve this during local development, you can tell Node.js to ignore TLS validation errors inside the sandbox using NODE_TLS_REJECT_UNAUTHORIZED=0.
⚠️ SECURITY WARNING: Disabling TLS validation bypasses certificate verification, which is a significant security risk. This workaround should only be used:
- For debugging TLS issues in isolated local development environments
- Never with real credentials or production endpoints
- Never in CI/CD pipelines or shared environments
Workaround: Pass Environment Variable via SBX
The sbx run command does not support the -e flag for environment variables. Use the two-step create + exec pattern instead:
# For debugging TLS issues only - never use with real credentials
# Step 1: Create the sandbox (or skip if it already exists)
sbx create --name debug-sandbox opencode .
# Step 2: Run with environment variable
sbx exec -e NODE_TLS_REJECT_UNAUTHORIZED=0 debug-sandbox opencode
Alternatively, combine into a single line:
sbx create --name debug-sandbox opencode . 2>/dev/null || true && \
sbx exec -e NODE_TLS_REJECT_UNAUTHORIZED=0 debug-sandbox opencode
Important: Only sbx exec supports the -e flag, not sbx run.
17. Chose "Open" Network Policy by Mistake
Symptoms
- Selected "Open" network policy during first sbx run
- Security concern: Open policy allows access to internal GSA resources
Root Cause
"Open" policy allows all network traffic without restrictions, which is a security risk on GFE (Government Furnished Equipment) machines.
Fix
Reset and reconfigure:
# Reset policy (will prompt for new choice)
sbx policy reset
# Choose "Balanced" when prompted, then add USAi
sbx policy allow network "api.gsa.usai.gov"
# Verify
sbx policy ls
Prevention
Always choose "Balanced" (Option 2) when prompted. The Balanced policy allows typical dev traffic while blocking internal network access.
18. Migrating from docker sandbox
Symptoms
- You're using deprecated
docker sandbox ...commands - Want to know the
sbxCLI equivalents
Root Cause
The Docker Desktop-integrated docker sandbox command is deprecated. The standalone sbx CLI replaces it and does not require Docker Desktop.
Fix
Migrate to the equivalent sbx commands:
| Deprecated Command | New Command |
|---|---|
docker sandbox create --name NAME opencode . | sbx create --name NAME opencode . |
docker sandbox run NAME | sbx run --name NAME |
docker sandbox exec NAME cmd | sbx exec NAME cmd |
docker sandbox ls | sbx ls |
docker sandbox rm NAME | sbx rm NAME |
Your existing sandboxes and secrets will continue to work with the sbx CLI.
19. OpenCode Shows Wrong Providers
Symptoms
- OpenCode lists generic providers instead of USAi
- Custom USAi model catalog missing
Root Cause
The USAi provider config is not loaded in the sandbox. With acq, it is
delivered by the usai-provider kit (applied by pinned remote reference
from the agentic-coding-patterns repo), which stages an opencode.jsonc at
~/usai-config/ and, at startup, merges it into OpenCode's global config at
~/.config/opencode/opencode.jsonc (the kit no longer sets OPENCODE_CONFIG).
acq applies it alongside the agentic-coding-playbook and
zscaler-ca-certificate kits. This symptom appears when the sandbox was created
without acq (so the kits were not applied), or with a plain backend run
(e.g. sbx run / msb run) without the kit refs.
Upgrading an existing sandbox (pre-kit). A sandbox created before the kit
migration has no USAi provider config and no playbook clone. acq attempts to
heal it in place: the next time you acq run against such a sandbox, it
detects the missing kit(s) and injects them (on sbx via sbx kit add). This
worked on sbx 0.35.0–0.37.x, where sbx kit add recreated the container with the
augmented kit set while preserving state.
sbx 0.38 caveat. On sbx >= 0.38,
sbx kit addno longer applies startup-bearing kits mid-life (see Section 35), and every built-in acq kit declares startup commands. In-place healing of the built-in bundle therefore does not work on 0.38+: to add or refresh the bundle you must recreate the sandbox (acq rm && acq run). acq detects the refusal and prints that guidance.Historical note. Earlier releases could not auto-heal in place: on
sbx≤ 0.34.x,sbx kit addfailed (failed to read tar header: unexpected EOF) on any kit shipping a static file — including theusai-providerkit (docker/sbx-releases#133).sbx0.35.0 fixed this, soacqrequiressbx>= 0.35.0 on the sbx backend and heals unconditionally.
acq waits for the sandbox to be ready for exec before probing (right after a
create or a cold start, exec fails with inspect exec: context deadline exceeded for a few seconds; acq polls until a trivial exec succeeds) so a
cold-start delay is not misread as "kit absent".
Detecting a pre-kit sandbox.
acqdecides a kit is missing by checking for the kit's footprint (e.g. the USAi config file), classifying on the probe's stdout (present/absent), never its exit status —test -fexits non-zero when the file is absent, which is indistinguishable from a genuine exec failure, so an exit-status check would wrongly treat "file absent" as "probe failed" and skip healing.
Fix
Usually there is nothing to do — re-running acq run opencode <path> against
the sandbox heals it in place (state preserved). If a kit injection fails, acq
prints the exact manual recovery command. You can also run it yourself; remote
kit sources must be allowlisted first (acq does this automatically, but by hand
on the sbx backend it is):
sbx settings set kit.allowedSources '["docker.io/","github.com/GSA-TTS/"]'
REPO="git+https://github.com/GSA-TTS/agentic-coding-patterns.git"
DIR="integrations/isolation/acq-kits"
sbx kit add SANDBOX "${REPO}#ref=<sha>&dir=${DIR}/usai-provider"
sbx kit add SANDBOX "${REPO}#ref=<sha>&dir=${DIR}/agentic-coding-playbook"
sbx kit add SANDBOX "${REPO}#ref=<sha>&dir=${DIR}/zscaler-ca-certificate"
As a last resort, recreate the sandbox from scratch (this discards the sandbox's session/context):
sbx rm --force SANDBOX # discard the sandbox (irreversible)
./acq run opencode /path/to/your/project
20. Authentication Failed After Copying a New Key
Symptoms
- USAi authentication fails immediately after creating/copying a key
- Key looks correct but is rejected
Root Cause
The displayed key value in the console may be truncated when selected by hand, so the stored secret is incomplete.
Fix
Regenerate the key and use the console copy button immediately instead of selecting the displayed text. Then confirm the secret is stored:
sbx secret ls -g | grep USAI_API_KEY
If problems persist, see Section 2 and Section 3. As a last resort, recreate the sandbox (this destroys all sandbox state, including uncommitted work):
acq rm <sandbox-name>
21. SBX Fails to Start with Host Path chdir Error
Symptoms
acq run opencode ...exits after printing an OCI runtime error- Error includes:
OCI runtime exec failed: chdir to '/Users/.../your-project': no such file or directory - The agent process may exit before opening an interactive session
Root Cause
SBX cached sandbox metadata can point at a workspace path that no longer exists or is not mounted inside the container. This is most likely after moving, renaming, or reprovisioning a project, or after reusing an old sandbox with a stale workspace path.
Fix
Find the stale sandbox and recreate it from the current workspace:
sbx ls
sbx rm <sandbox-name>
./acq run opencode /path/to/your/project
For this quickstart clone itself, the default sandbox name is derived as
<agent>-<clone-folder> — for ./acq run opencode . in a clone named
agentic-coding-quickstart that is opencode-agentic-coding-quickstart:
sbx rm opencode-agentic-coding-quickstart
./acq run opencode .
If you used an explicit sandbox name, remove that same name and rerun with the same --name value:
sbx rm my-project
./acq run --name my-project opencode /path/to/your/project
22. Signed Commits Show "Unverified" on GitHub
Symptoms
- Commits made inside a sandbox are signed (
git log --show-signaturelooks fine locally) but GitHub shows an Unverified badge, or no badge. acqprinted a note before attaching: "no repo-local git user.email is set for this project."
Root Cause
The git-ssh-sign kit signs commits, but GitHub only marks an SSH-signed commit
Verified when both are true:
- the commit's
user.emailis an email verified on your GitHub account, and - your public signing key is registered on that account as a Signing Key (Settings → SSH and GPG keys → New SSH key → Key type: Signing Key) — an authentication-only key does not verify commits.
No kit sets user.email / user.name (identity is user-owned, not something a
signing mixin should inject). Crucially, the sandbox has its own home
directory, so your host's global ~/.gitconfig identity is not visible
inside it — only the project's repo-local identity (stored in the mounted
workspace) reaches the sandbox. A host global user.email alone therefore yields
signed-but-Unverified commits.
Fix
Set the identity repo-local (inside the project, so the mount carries it into
the sandbox — a host --global value will not):
git config user.email you@verified-on-github.example
git config user.name "Your Name"
Then register the public half of your signing key on GitHub as a Signing
Key, and make a new commit — verification applies going forward. acq
checks the project's repo-local user.email before attaching (the only tier
the sandbox can see) and warns if it is unset. For signing mechanics and more
failure modes, see the kit's
TROUBLESHOOTING.md.
The end-to-end verification/identity gap in the kits themselves is tracked upstream in agentic-coding-patterns#211; the quickstart-side decision (docs + advisory) is recorded in ADR-0007.
Backend note (
msb): thegit-ssh-signkit works on both thesbxandmsbbackends.sbxforwards the host ssh-agent into the sandbox implicitly wheneverSSH_AUTH_SOCKis set.msb0.6.9 shipped--vsock(superradcompany/microsandbox#1297), andacqnow forwards the host ssh-agent into the msb guest automatically whenSSH_AUTH_SOCKis set (via--vsockplus an in-guestsocatbridge, see ADR-0021), sogit-ssh-signworks on msb at parity with sbx. Verified end-to-end on a macOS/HVF host (2026-08-17) viascripts/verify-backends(the guest's forwarded agent exposes a hermetic throwaway key over the--vsock+ socat path); re-run on the ADR-0011 periodic-validation cadence. The upstream watch-list and follow-onacq/kit checklist are tracked inGSA-TTS/agentic-coding-quickstart#303.
23. USAi 401 in an Existing Sandbox After Deleting/Recreating the Global Secret
Symptoms
-
A fresh sandbox authenticates to USAi fine, but an existing sandbox keeps failing with HTTP 401 from the models API.
-
You recently deleted the global USAi secret and re-added it (rather than using
acq usai-rotate-api-key), and/or you had a strayUSAI_API_KEYexported in your shell (.zshrc/.bashrc) that you commented out. -
acq runprinted something like:The global USAi key works in a fresh sandbox, but 'opencode-workspace' still fails with HTTP 401. This usually means the existing sandbox has a stale USAI_API_KEY placeholder from before rotation. Could not read the sandbox's USAI_API_KEY placeholder. Aborting attach. -
The
usai-providerconfig (opencode.jsonc) is loaded — OpenCode shows the USAi provider and correctbaseURL— so this is not the pre-kit migration case in Section 19. The config is fine; only the injected credential fails to resolve.
Root Cause
USAi is injected as a custom secret (sbx secret set-custom), which is
not covered by sbx's built-in provider proxy automatically — but it is still
proxied via the placeholder mechanism. sbx bakes a placeholder token into
each sandbox's USAI_API_KEY at creation time, and the sbx proxy resolves that
placeholder to the real global secret value at request time (so the agent's env
holds the placeholder, not the raw key — see
Section 14).
When you delete and re-add the global secret (as opposed to rotating it in
place), sbx mints a new placeholder. A newly created sandbox picks up the new
placeholder, but an existing sandbox still carries the old placeholder —
which the proxy can no longer resolve. The proxy then injects an empty
USAI_API_KEY, so USAi returns 401. Reading the sandbox's baked-in value can
even come back empty, which is why older versions hit a hard
Could not read the sandbox's USAI_API_KEY placeholder. Aborting attach.
dead-end here.
Contrast with
acq usai-rotate-api-key(scripts/rotate-apikey), which preserves the existing placeholder across rotation on purpose — so running sandboxes keep resolving. Deleting + re-adding the secret defeats that.
Fix (automatic)
On attach, acq run validates the sandbox's USAi key (check_key) and, when it
is not 200, prints the rotate steps and offers to rotate the key in place
(acq usai-rotate-api-key), then re-validates before attaching. Rotation
preserves the placeholder, so this resolves the common expired-key case:
./acq run opencode /path/to/your/project
Note (stale-placeholder recovery). The deeper two-route recovery for this exact 401-after-delete-and-re-add case — session-preserving recreate, or a non-destructive sandbox-scoped rebind to the current global placeholder — is not implemented in
acq(see ADR-0008 for the design). If an in-place rotate does not clear the 401 (because the stored placeholder — not the key value — is stale), use the manual rebind below or recreate the sandbox.
Fix (manual)
Either recreate the sandbox:
sbx rm --force <sandbox-name>
./acq run opencode /path/to/your/project
…or rebind the existing sandbox to the current global placeholder. First read the current placeholder from the global secret list, then bind a sandbox-scoped secret to it (sbx prompts for the key value):
# The value in the USAI_API_KEY column is the current placeholder:
sbx secret ls -g | grep USAI_API_KEY
sbx secret set-custom <sandbox-name> --host api.gsa.usai.gov \
--env USAI_API_KEY --placeholder <current-placeholder>
Do not bind to the sandbox's old placeholder — that is the value that stopped resolving. Always bind to the current global one.
Prevention
- Rotate with
acq usai-rotate-api-key, which preserves the placeholder so existing sandboxes keep working. Avoid deleting + re-adding the global secret. - Remove any
export USAI_API_KEY=...from your shell profile — the sandbox gets its value from sbx injection, and a host env var only causes confusion.
Related
- Section 3 — Agent Cannot See API Key / USAi Authentication Fails
- Section 14 — SBX Built-in Proxy Doesn't Auto-Cover USAi's Custom baseURL
- Section 20 — Authentication Failed After Copying a New Key
- Decision record: ADR-0008
24. Pulled/Switched a Branch but acq Still Shows Old Behavior
Symptoms
- You
git pullorgit checkouta branch with a fix, butacqstill prints wording or behaves in a way that only exists in an older version. git pull origin <branch>printsAlready up to date.yet nothing changes.- You are
cd'd into a quickstart clone, but the behavior does not match the code you see in that clone'sacq.
Root Cause
Two independent traps, often combined:
-
git pull origin <branch>does not switch you to that branch.pull=fetch+merge FETCH_HEADinto the branch you are currently on.Already up to datemeans the merge was a no-op — not that your working tree now contains the branch. If you never rangit switch <branch>/git checkout <branch>, your working tree still has the oldacq. -
An
acqon yourPATHpoints at a different clone. Ifacqis invoked by bare name (not./acq), the shell resolves it viaPATH. A symlink in~/binor/usr/local/binmay point at a different clone than the one you edited/pulled.acqfollows that symlink to find its own directory, so it runs the other clone's code — you update clone A and execute clone B.
A related variant: exporting QUICKSTART_CLONE overrides where sibling helper
scripts are located, which can also make "which clone is in effect" confusing.
Fix
First, ask acq which file and clone are actually running:
acq version
It prints the resolved script path, the clone directory, and that clone's git
branch@commit (and flags if the tree is dirty or QUICKSTART_CLONE is set).
Compare the branch/commit against what you expect.
Then, depending on which trap you hit:
-
Wrong branch checked out: switch (don't just pull):
git switch fix/your-branch # or: git checkout fix/your-branch git rev-parse --abbrev-ref HEAD # confirm -
Running a different clone via a PATH symlink: either run the clone you updated directly, or re-point the symlink:
# Run the clone you actually updated: ./acq run opencode <your-project> # …or see where the installed one lives and re-point it: readlink -f "$(command -v acq)"
When you run acq run from inside one quickstart clone while the executing
acq lives in another, acq now prints a startup note pointing this out.
Prevention
- Use
git switch <branch>to change branches; treatAlready up to dateas a signal to checkgit rev-parse --abbrev-ref HEAD, not confirmation. - Keep a single canonical clone, and make any
acqonPATHa symlink to that clone'sacq. Runacq versionwhen in doubt.
25. acq run Prompts for a GitHub Username/Password During Kit Fetch
Symptoms
Running acq run … prints an interactive prompt and then fails:
Username for 'https://github.com': you@agency.gov
Password for 'https://you%40agency.gov@github.com':
kit-translate: failed to fetch git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=…&dir=…
kit-translate: remote: Invalid username or token. Password authentication is not supported for Git operations.
You may already be authenticated with the gh CLI (gh auth status is green).
Root Cause
gh auth login authenticates the gh CLI, not plain git. The acq kit
fetch uses git directly. If your machine has a global git credential helper or
a url.<x>.insteadOf rewrite (common in enterprise/egress setups), git tries to
authenticate to the kit source and — failing — drops into an interactive
prompt (and GitHub disabled git password auth in 2021, so it can't succeed).
Fix
Wire git to use your gh token, once:
gh auth setup-git
If it still prompts, you likely have a rewrite forcing auth on the clone — inspect it with:
git config --global --get-regexp 'url\..*insteadOf'
Prevention
As of #207, acq's kit fetch is non-interactive: it sets GIT_TERMINAL_PROMPT=0
and first attempts an anonymous fetch with any inherited credential helper /
github.com insteadOf rewrite neutralized (so an unauthenticated fetch
proceeds without prompting), then retries once with your system git config
(still prompt-disabled) for sources that require auth (enterprise mirror, etc.).
It can no longer hang on a
prompt; a genuine failure now prints this remedy.
Related
- Issue #207 (non-interactive fetch), #208 (docs + loud fallback).
27. 500 failed to create network After a Host Restart (sbx)
Numbered 27 to avoid colliding with entry 26 (macOS Gatekeeper /
mkfs.erofs), which is landing in a separate change.
Symptoms
A sandbox that worked before is refused after a host reboot/restart. The image pull succeeds ("Already exists" / "Image is up to date"), then sandbox creation fails:
Status: Image is up to date for docker/sandbox-templates:opencode-docker
ERROR: request failed: 500 Internal Server Error: failed to create network
(Sometimes with the daemon detail Error response from daemon: already exists.)
./acq run … and a direct sbx run … fail identically — acq only passes the
command through to sbx.
Root Cause
This is an sbx daemon-lifecycle bug, not a Quickstart / acq / msb issue
and not a network-policy problem. On an unclean shutdown/reboot, sbx's daemon
(sandboxd) does not tear down a sandbox's network, so a stale network record
survives the restart. On the next sbx run, sandboxd tries to (re)create that
network, hits the leftover entry, and returns 500 … already exists. The
successful image pull just before the error is unrelated — the failure is the
network-create step that follows it.
Tracked upstream (already open — no new report needed):
- docker/sbx-releases#181 —
500 failed to create network: … already exists. - docker/sbx-releases#353 —
sandboxd crash during dead-VM cleanup orphans a sandbox; the only documented
recovery is a full
sbx reset.
Fix
Recover least-destructive first:
-
List and clear the stale sandbox. See what
sbxstill thinks exists, then remove the orphaned one and retry:sbx ls sbx rm <name> --force # <name> from the list; stop it first if running ./acq run opencode <path>(If
sbx rm --forcereports "not found" but the name is still claimed, that is docker/sbx-releases#129 — go to step 2.) -
Full reset, preserving secrets — the recovery the upstream issues cite. This clears stale daemon/network state but keeps your USAi/GitHub secrets so you do not redo secret setup:
sbx reset --preserve-secretsAfter a reset you may need to re-apply the network policy before running:
sbx policy init balanced sbx policy allow network "api.gsa.usai.gov" ./acq run opencode <path>
Prevention / Status
- This is an upstream
sbxdaemon-shutdown bug; we cannot fix it in this repo. Follow docker/sbx-releases#181 / #353 for the daemon-side fix. - Until then,
sbx reset --preserve-secretsis the reliable recovery after a restart leaves a sandbox unusable. (--preserve-secretsavoids re-running the Step 3 secret setup.)
28. msb May Auto-Run a --script-path-Registered Script at Boot (Re-Verify on Version Bump)
Symptoms
Not a bug you hit today — a latent failure to watch for after a microsandbox
(msb) version bump. If a future msb release starts auto-executing a registered
--script-path script at guest boot, kit startup commands could run twice
on a resume: once from microsandbox's own boot replay, and again from acq's
start/restart heal (which re-runs the startup phase via msb exec).
Root Cause
The acq msb adapter stages kit startup commands as a create-time script
registered with --script-path acq-startup:<hostfile> (ADR-0017). This is only
safe/runtime-neutral because, on the pinned msb version, a bare --script-path
registration merely places the script on the guest PATH
(/.msb/scripts/acq-startup) — microsandbox does not auto-run it at boot.
Boot replay is driven only by a persisted LaunchConfig.startup
(runtime.entrypoint/runtime.cmd), which acq never populates. This was
confirmed against microsandbox source (crates/cli/lib/commands/start.rs,
restart.rs, Sandbox::start_detached()).
This is a load-bearing, unverifiable-from-this-repo assumption: it depends on microsandbox internals that could change in a future release.
Prevention / Re-Verification
On each msb version bump (part of the quarterly re-verification cadence), confirm
--script-path boot-inertness still holds:
# Create a sandbox with a sentinel startup command staged via --script-path,
# then stop + start it WITHOUT going through acq, and check the sentinel ran
# 0 times (inert) rather than 1 (auto-run at boot):
msb create --name inert-probe --script-path acq-startup:/path/to/sentinel.sh <image>
msb stop inert-probe && msb start inert-probe
# inspect: the sentinel's side effect must NOT have fired on the bare start
msb rm inert-probe
If a future msb does auto-run the registered script at boot, the adapter must
stop double-running startup — e.g. drop the acq start/restart heal's
startup re-run for msb, or stop staging the script. See the "VERIFIED NEUTRALITY
ASSUMPTION" note in _acq_msb_stage_startup_script (acq.backends/msb.sh).
Related
- ADR-0017 (msb create-time startup-script staging).
29. opencode-ai's postinstall script was not run on a Fresh sbx Sandbox
Symptoms
A newly created sbx opencode sandbox fails at launch (the image pull
succeeds, then the agent exits 1):
Error: opencode-ai's postinstall script was not run.
This occurs when using --ignore-scripts during installation, or when using a
package manager like pnpm that does not run postinstall scripts by default.
To fix this, run the postinstall script manually:
cd node_modules/opencode-ai && node postinstall.mjs
Often appears right after a fresh image pull. Seen with opencode-ai@1.18.12 on
macOS/Apple Silicon, sbx v0.37.1.
Root Cause
This is in the Docker sandbox-templates:opencode-docker image, not in
acq, the quickstart, or your local setup. On the sbx backend opencode is
baked into that image (acq does not install it — that only happens on the msb
backend). Since opencode v1.15.1, the opencode-ai npm package needs its
postinstall step to fetch the actual platform binary. The image appears to
install opencode-ai at build time with lifecycle scripts skipped (e.g.
npm ci --ignore-scripts, or pnpm/bun which skip postinstall by default), so the
binary is missing in the shipped image and opencode's guard fires on first run.
Evidence it is a build-time script-skip, not your environment (run inside the
sandbox): npm config get ignore-scripts returns false (so nothing is
disabling scripts at runtime), yet npm ls -g opencode-ai shows the package
present without a working binary — the postinstall simply never ran when the
image was built.
Not a supply-chain compromise and not related to the recent quickstart changes; a fresh image pull just fetched a newer opencode build with this gap.
Fix
As of the current acq, acq run opencode <path> remediates this
automatically: before attaching an opencode agent, acq probes
opencode --version in the sandbox and, if the binary is not functional, runs
the package's postinstall.mjs (on either backend), then attaches. So the
normal flow should now just work; if it still fails, apply the manual fix below.
Apply the manual fix with sbx exec, not sbx run. sbx run (the raw
backend command, without acq's remediation) launches opencode, which
re-hits the missing-binary guard and exits before you can do anything. sbx exec runs a command in the sandbox without launching the agent, so it works
even when a raw run fails.
# 1. Find the sandbox that was created (its name, e.g. opencode-<project>):
sbx ls
# 2. Run opencode's postinstall inside it via sbx exec (no agent launch):
sbx exec <sandbox-name> -- sh -c 'cd "$(npm root -g)/opencode-ai" && node postinstall.mjs'
sbx exec <sandbox-name> -- opencode --version # confirm the binary now runs
# 3. Now attach normally:
./acq run opencode <path> # or: sbx run <sandbox-name>
If sbx exec reports the sandbox is not running, start it first
(sbx start <sandbox-name>) then re-run the exec.
Note: a raw
sbx run <sandbox-name>(bypassing acq) just re-triggers the error. Preferacq run, which remediates automatically; thesbx execpath above is the manual fallback if remediation ever fails.
Prevention / Status
acq run opencodenow runs the postinstall automatically before attach (both backends), so the common case is handled without user action.- The underlying image gap is built and shipped by Docker; we cannot fix it in this repo. An upstream report has been filed: docker/sbx-releases#395. Related upstream: anomalyco/opencode#27906 (opencode requires postinstall to fetch its binary since v1.15.1).
- If acq's automatic remediation ever fails, the manual
node postinstall.mjsabove is the reliable per-sandbox workaround. - The msb backend installs opencode itself (
npm install -g, scripts enabled) rather than using this image, so it is a possible alternative if the sbx image stays broken — though it may hit the same opencode packaging issue.
30. Sandbox can't reach USAi / GitHub / npm — three signatures, three fixes
Symptoms
Early in acq run, an outbound request from inside the sandbox fails and (before
this was disambiguated) acq could mis-report it as an expired key and prompt to
rotate:
agentic-coding-playbook: fetch of GSA-TTS/agentic-coding-playbook@… tarball failed
curl: (56) OpenSSL SSL_read: … unexpected eof while reading, errno 0
Your USAi API key looks invalid or expired (HTTP …000 from the models API).
Rotating the key never helps here — the failure is in the network path, not the key. But "the sandbox can't reach the network" is not one problem: three distinct failures share overlapping symptoms and need different remedies. Identify which one you have before acting.
Triage — which signature is it?
| Signature (from inside the guest) | Cause | Fix |
|---|---|---|
Broad curl (56) unexpected eof / HTTP 000 on multiple hosts at once (USAi and GitHub and npm) | Corrupted / stale msb state (not a clean-install behavior) | Wipe msb data + reinstall, then msb doctor (below) |
USAi only fails with NXDOMAIN / curl rc=6 (or a WAF-gated API such as GitLab's /api/v4/* returns 403 from awselb/2.0), while GitHub + npm work fine | Split-horizon DNS — the host resolves the name to a private tunnel address (100.64.x on ZPA); a public resolver returns the public endpoint, which the WAF rejects, and msb's rebind protection drops the private answer | Let the guest follow the host's resolvers with rebind protection off, which acq does automatically on a host whose resolvers are in 100.64/10: unset ACQ_MSB_DNS_NAMESERVER and ACQ_MSB_DNS_REBIND_PROTECTION, or set the latter to 0 if the sandbox was created while the tunnel was down (below) |
| A genuinely intercepted endpoint fails its TLS handshake behind a corporate proxy that terminates it | The proxy's root CA isn't trusted on msb's upstream (proxy→server) leg | acq passes it via --tls-upstream-ca-cert (defense-in-depth; below) |
Probe from inside the sandbox to read the signature — the curl exit code is the
discriminator (6 = didn't resolve; 35/56 or 000 = resolved but the
connection was cut):
# msb
msb exec <sandbox> -- curl -sS -o /dev/null -w '%{http_code}\n' -v https://api.gsa.usai.gov/api/v1/models
msb exec <sandbox> -- curl -sS -o /dev/null -w '%{http_code}\n' -v https://api.github.com
# sbx
sbx exec <sandbox> -- curl -sS -o /dev/null -w '%{http_code}\n' -v https://api.gsa.usai.gov/api/v1/models
For a deeper host-side characterization (interception vs egress path, DNS, host-vs-host comparison), run the bundled read-only probes:
./scripts/diagnose-tls-intercept # classify interception / cert-trust / egress path
./scripts/diagnose-host-egress-diff # compare a failing host to a working one
Signature 1 — broad unexpected eof on multiple hosts (stale msb state)
Observed in the field on a Zscaler-enrolled macOS host: every outbound HTTPS
call from the guest failed at once with curl (56) unexpected eof. It did not
reproduce on a clean install — a machine with the same Zscaler configuration
worked fine — and it was cleared by a full msb data wipe + reinstall. The
matching upstream report (superradcompany/microsandbox#1344)
was closed as non-reproducible: something in the local msb state was corrupt,
not a defect in a clean msb.
Fix: wipe msb's data/state and reinstall, then confirm host readiness:
# Reinstall msb (removes and re-lays its runtime state)
curl -fsSL https://install.microsandbox.dev | sh
# Verify host virtualization + runtime prerequisites; --fix applies supported setup
msb doctor
msb doctor --fix
If a broad unexpected eof recurs on a clean install, that would be new
evidence of an msb bug — capture the diagnose-* output and reopen
superradcompany/microsandbox#1344.
Signature 2 — split-horizon DNS (USAi NXDOMAIN, GitLab API 403)
On a GFE/Zscaler host, GitHub and npm resolve and return 200 from the guest,
but api.gsa.usai.gov fails to resolve (curl rc=6), or a WAF-gated API
behind the same tunnel (GitLab's /api/v4/*) answers 403 from awselb/2.0
while its git endpoints work. Both are the same mechanism. The host's Zscaler
resolvers answer these names with private ZPA addresses (100.64.x) that
ride the tunnel's allow-listed path; a public resolver answers the public load
balancer, which the WAF rejects. And even with the host resolver, msb's DNS
rebind protection drops private-range answers, so the name resolves to nothing.
acq's defaults handle this: the guest follows the host's resolvers (no
--dns-nameserver), and when a host resolver is in 100.64/10 (the ZPA
signature) msb is created with --no-dns-rebind-protection, with a one-line
notice on stderr. If you see this signature, check that neither knob is
overriding the defaults, and that the sandbox was not created while the tunnel
was down (detection then sees no 100.64/10 resolver and keeps the protection):
unset ACQ_MSB_DNS_NAMESERVER # a forced public resolver reintroduces the 403/NXDOMAIN
unset ACQ_MSB_DNS_REBIND_PROTECTION # =1 drops the private tunnel answers
ACQ_MSB_DNS_REBIND_PROTECTION=0 acq … # recreate: force it off if detection missed
The setting is applied at create time; recreate the sandbox after changing it.
Keeping rebind protection on is a deliberate trade: it stops a kit-allowed
domain from being resolved into private space by an attacker-influenced DNS
answer, and it also stops every tunnel-only host from resolving at all. See the
DNS section of docs/BACKEND_GUIDE.md for the detection rule and the
mitigations that remain with it off.
Verified on msb 0.6.16: from the guest, gitlab.login.gov resolves to its ZPA
address, /api/v4/user with the bound token returns 200, and the USAi models
endpoint returns 401 (reachable) instead of failing to resolve. The guest's
public egress IP is unchanged (still the Zscaler cloud). Earlier text here
reported the internal resolver as unreachable from the guest; that predates
msb's host-side network stack and no longer applies.
acq reports this distinctly ("did not RESOLVE … likely a split-horizon name")
rather than as a generic network cut or a bad key.
Signature 3 — genuine TLS interception (upstream CA trust, defense-in-depth)
When a corporate proxy (Zscaler, Netskope, …) genuinely terminates an
endpoint, acq's --tls-intercept (which msb needs to substitute injected secrets
on the wire) makes msb's host-side proxy re-originate an upstream TLS connection
to that proxy — which presents a corporate-signed leaf. That upstream leg must
trust the corporate root. --trust-host-cas does not help (it only ships CAs
into the guest, not into the proxy's upstream verifier), and msb's native-cert
loader surfaces enterprise roots unevenly on macOS. When the root isn't surfaced,
the upstream handshake fails after the guest-facing TLS completed and the guest
sees unexpected eof.
acq passes the host's root CAs to that upstream verifier via
--tls-upstream-ca-cert whenever interception is on, so a genuinely-terminated
endpoint verifies even when native-cert loading would miss the root. Controls:
ACQ_MSB_UPSTREAM_CA_CERT=/path/to/root.pem— trust an explicit PEM (colon- or space-separated for several). Highest precedence; use it on non-macOS hosts or when you already have the root on disk.ACQ_MSB_UPSTREAM_CA_AUTODETECT=0— disable the macOS auto-export of the host search-list roots (on by default).ACQ_MSB_NO_UPSTREAM_CA=1— reproduction/testing only: withhold the upstream CA even when available.
This is defense-in-depth for the interception case only — it does not address Signature 1 (stale state) or Signature 2 (split-horizon DNS).
Prevention / Status
acqclassifies an in-guest curl result and reports the matching diagnosis:unresolved(DNS/split-horizon) vsunreachable(connection cut) vs a real HTTP status — never "invalid or expired" for a network failure, and it does not prompt to rotate a good key. It fails closed (aborts the run) rather than attach a session that cannot reach USAi.- The in-guest
npm installfailure message is likewise disambiguated: it distinguishes a genuinely-missing npm from an unreachable / unresolvable registry, so a network cut is not misread as "node isn't installed." - The built-in kits are ordered so
zscaler-ca-certificateis applied first, so guest-side CA trust is established before the other kits make network requests (this matters on the sbx backend, whose kits apply sequentially). - Making the sandbox's outbound TLS traverse a corporate proxy, and giving the guest a resolver that can see internal zones, are host/network-configuration matters — resolve them with your network/endpoint administrators where acq's tunables don't cover your environment.
26. macOS Gatekeeper Blocks mkfs.erofs at sbx policy init (PATH shadowing)
Symptoms
On macOS (seen on Tahoe / macOS 26, Apple Silicon), sbx policy init balanced
— the first command that builds a sandbox filesystem — fails when macOS blocks a
helper binary:
"mkfs.erofs" cannot be opened because the developer cannot be verified.
macOS cannot verify that this app is free from malware.
xattr -d com.apple.quarantine /opt/homebrew/bin/mkfs.erofs does not clear
it — even run with sudo. It only happens on machines that also have Homebrew
erofs-utils installed; users without it never see the block.
Root Cause
This is an sbx PATH-resolution bug, not a Quickstart / acq / msb
issue, and not a supply-chain compromise. sbx ships its own notarized
mkfs.erofs, but invokes it via $PATH, so a Homebrew erofs-utils shadows the
bundled copy:
- sbx v0.37.1 bundles a notarized
mkfs.erofsat/opt/homebrew/Caskroom/sbx/<version>/libexec/mkfs.erofs(spctl→ accepted; Developer ID: Docker Inc (9BNSXJN65R)). - If Homebrew
erofs-utilsis also installed,/opt/homebrew/bin/mkfs.erofs(a symlink intoCellar/erofs-utils/…) is earlier on$PATH. That binary is ad-hoc-signed / not notarized (spctl→ rejected,Signature=adhoc, no TeamIdentifier). - sbx resolves
mkfs.erofsfrom$PATH, picks up the unnotarized Homebrew binary instead of its own, and macOS Gatekeeper rejects it. - There is no
com.apple.quarantinexattr on the rejected binary — the block is the code-signing / AMFI (syspolicyd) assessment, which is why removing the xattr does nothing.
The erofs-utils Homebrew formula itself is fine (kernel.org source,
SHA256-pinned bottles). The problem is purely that sbx's PATH lookup picks the
unnotarized system copy over its own notarized bundled one.
Fix
Make sbx use its own notarized binary — remove the shadowing Homebrew copy from PATH:
brew uninstall erofs-utils # if nothing else needs it; sbx then uses its bundled, notarized mkfs.erofs
If you cannot remove it, approve the Homebrew binary via System Settings →
Privacy & Security → "Allow Anyway", then re-run the command and click "Open"
on the pop-up. Do not rely on xattr -d com.apple.quarantine — it does not
satisfy the code-signing gate on an unnotarized binary.
Prevention / Status
- The durable fix is upstream in
sbx: invoke the bundledlibexec/mkfs.erofsby absolute path rather than resolvingmkfs.erofsfrom$PATH. The bundled binary is already notarized, so no notarization change is needed. Tracked at docker/sbx-releases#392. - Until sbx pins the path,
brew uninstall erofs-utils(or the "Allow Anyway" fallback) unblocks affected machines.
31. msb Balanced-Egress Host List Drifts from the sbx balanced Policy
Symptoms
On the msb backend a host that works on sbx (which uses the balanced
policy) is refused from inside the sandbox — a curl/package-manager fetch fails
to connect, or an install that succeeds on sbx times out on msb. Conversely, the
msb host list may reference hosts sbx balanced has since removed.
Root Cause
The msb backend mirrors the sbx balanced egress set from a vendored,
point-in-time snapshot at acq.backends/msb-balanced-hosts.txt (see ADR-0018).
sbx generates its balanced policy in the daemon; that list is not stored in
this repo, so when sbx updates balanced, the vendored mirror goes stale until a
human re-syncs it. (This is a deliberate tradeoff — the alternative, shelling out
to sbx policy inspect at msb-create time, requires sbx installed on an msb-only
host and couples the backends.)
Fix / Re-sync
Re-sync the vendored file from a machine that has sbx with the balanced policy:
# 1. Inspect the active sbx policy and copy the network `allow` host:port rows.
sbx policy inspect local-policy
# 2. Diff them against the vendored mirror (network rows only; NOT the
# filesystem:read/write rows).
$EDITOR acq.backends/msb-balanced-hosts.txt
Copy each allow … network host:port row verbatim into the matching group in
the file (do not pre-translate wildcards or ports — the msb adapter's
_acq_msb_balanced_rules_into does that). Commit the diff. The offline test
suite (scripts/test-acq-bats) re-parses the real file and fails if any line is
malformed, so run it after editing.
As a stopgap for a single missing host, either add it to a site-specific list and
point ACQ_MSB_BALANCED_HOSTS_FILE at it, or (for a one-off) create with an extra
--net-rule allow@<host>:tcp:443.
Prevention / Status
- Re-verify on the quarterly review cadence (per
AGENTS.mdperiodic re-verification): re-runsbx policy inspect local-policyand diff the network rows againstacq.backends/msb-balanced-hosts.txt. - A future option (ADR-0018 "Alternatives considered") is to move the list into a shared neutral kit consumed by both backends, removing the manual re-sync.
32. msb create/run Fails: the dns target supports tcp, udp, or any, not dns
Status: RESOLVED for supported versions. microsandbox 0.6.9 shipped the upstream release-build parser fix for the
allow@dnsmacro, and acq now requires msb >= 0.6.9 (MIN_MSB_VERSIONinacq.backends/msb.sh). The msb adapter emits the nativeallow@dnsmacro for the gateway-DNS grant again; the expandedallow@host:udp:53+allow@host:tcp:53workaround has been removed. Because the floor fails closed on anything older than 0.6.9, a supported acq install can no longer hit this failure. The reproduction and root-cause history below is retained for context.
Symptoms (historical, msb <= 0.6.8)
On the msb backend, acq run/acq create (or a raw msb create/msb run)
aborts during "Creating sandbox …" with:
error: the `dns` target supports `tcp`, `udp`, or `any`, not `dns`
acq(msb): error: 'msb create' failed for '<sandbox>'.
Reproduces with the minimal case on a released msb (confirmed on msb 0.6.8):
msb run --net-default deny --net-rule "allow@dns" alpine -- true
Root Cause (historical, msb 0.6.7–0.6.8)
An upstream msb bug in the --net-rule parser, not an acq misconfiguration.
msb's semantic allow@dns macro was parsed by advancing past the dns target
token inside a debug_assert_eq!(parts.next(), Some("dns")). debug_assert! is
compiled out of a release binary, so in the shipped msb the iterator was
never advanced past dns; the same dns token was then read as the protocol
field, which only accepts tcp/udp/any — hence the error. A bare
allow@dns therefore hard-failed on release builds from 0.6.7 through 0.6.8
(fixed in 0.6.9; see Fix below), even though the macro was documented as valid.
The balanced-egress baseline (ADR-0018) needs a gateway-DNS grant under
--net-default deny, and originally emitted allow@dns, tripping this bug on
pre-0.6.9 release builds.
Fix
microsandbox 0.6.9 fixed the parser: the target-iterator advance was moved
OUT of the debug_assert_eq!, so it now runs on release binaries too and a bare
allow@dns parses correctly. acq raised MIN_MSB_VERSION to 0.6.9 and
collapsed the former expanded workaround back to the native macro:
--net-rule allow@dns
This is emitted in _acq_msb_balanced_rules_into (and the strict-tier path) in
acq.backends/msb.sh. The earlier workaround emitted the expanded equivalent
(--net-rule allow@host:udp:53 --net-rule allow@host:tcp:53) so that pre-0.6.9
release builds could still resolve DNS under a deny-default; that is no longer
needed now that the floor guarantees a fixed parser. If you script raw msb
calls against a 0.6.9+ binary, allow@dns is safe to use directly.
Prevention / Status
- Resolved. The version floor (
MIN_MSB_VERSION=0.6.9) fails closed on any older binary before a create is attempted, so acq cannot reach this failure on a supported install. If a future regression is suspected, re-run the minimalmsb run … --net-rule "allow@dns"case above on the quarterly review cadence to confirm the macro still parses.
33. --kit Services Dead After a Resume/Reboot on msb — Ports Mapped, Nothing Listening
Symptoms
On the msb backend, after a host reboot (or any acq stop/acq start,
acq restart, or acq run <existing-sandbox> that resumes a stopped sandbox),
a sandbox created with an extra CLI kit — e.g.
acq run opencode --kit …/acq-kits/openchamber … — comes back with the kit's
published ports still mapped, but nothing answering behind them:
$ acq ports opencode-agentic-coding
sandbox 3000 -> host 127.0.0.1:3000 (create-time -p)
sandbox 4096 -> host 127.0.0.1:4096 (create-time -p)
$ opencode attach http://127.0.0.1:4096
Error: Unable to connect. Is the computer able to access the url?
An acq exec <sandbox> -- ps -ef shows no opencode serve, no openchamber,
and — the tell — no supervisor: respawn loops at all. This is distinct from a
startup race (§ the wrapper's "shared server not answering yet on :4096"),
where the supervisor processes ARE present and merely still warming up: here the
supervisors were never (re-)launched, so the ports forward into a guest with the
kit's services entirely absent.
Root Cause
A kit's startup-phase commands (which, for the openchamber kit, launch the
supervised shared opencode serve on :4096 and the OpenChamber UI on :3000; for
the paseo kit, the daemon supervisor on :6767) are re-run on a resume only by
acq's heal (acq_backend_ensure_kits_applied) — msb start alone does not replay
them (ADR-0017). Two compounding gaps kept those services dead on resume:
-
The heal skipped CLI
--kitkits. It re-applied only the built-in kits and anyACQ_EXTRA_KITS; it did not fold in kits supplied on the command line via--kit(ACQ_CLI_KITS). Fixed by appendingACQ_CLI_KITSto the heal's kit list — but that only helps when the array is populated. -
The
--kit/extra refs were never persisted.ACQ_CLI_KITSis populated byextract_kit_flagsin therun/createdispatch arm only, andACQ_EXTRA_KITSis a bare environment variable. A lateracq start/acq restart(or a name-onlyacq run <sandbox>) does not re-parse--kitand may run in a shell that never exportedACQ_EXTRA_KITS, so the heal ran with an empty CLI/extra set and re-ran only the built-ins' startup. The provision path does fold--kitin, so the create-time-pport mappings are part of the persisted sandbox config and are restored by msb on start — but the heal skipped the kit whosestartupphase brings the services up. Result: ports restored, services not — exactly the "mapped but dead" state above.
Fix
Both gaps are closed:
acq_backend_ensure_kits_applied(msb) appendsACQ_CLI_KITSto the heal's kit list, matching howacq_backend_provisionassembles the kit set.- acq now persists the CLI (
--kit) andACQ_EXTRA_KITSrefs host-side at provision (a small*.kitsrecord beside the bundle-provenance record, keyed by backend + sandbox name) and reloads them in theacq start/acq restartverbs and on a name-onlyacq run <sandbox>re-attach, before the heal. So amsb stop+acq startcycle now re-runs a--kitkit's startup automatically — you no longer have to re-pass--kit. The record's presence is authoritative: a sandbox created with no CLI/extra kits reloads nothing, and a legacy sandbox with no record behaves exactly as before (reload is a no-op).
To recover a legacy sandbox (created before this fix, so it has no persisted
record) without recreating it, resume it once with the SAME --kit ref you
created it with, which both re-runs the kit's startup and writes the record for
next time:
acq run opencode --kit …/acq-kits/openchamber <path> # heals + re-runs kit startup
# or, if you don't need to attach:
acq restart <sandbox> # on a sandbox created by a fixed acq, this now
# restores CLI-kit startup on its own
If a resumed sandbox is already up but its services are dead, the quickest manual kick is to re-run the kit's startup script directly:
acq exec <sandbox> -- sh /home/agent/openchamber-start.sh &
# (paseo kit: sh /home/agent/paseo-start.sh)
Prevention / Status
- Fixed in
acq.backends/msb.sh(heal folds CLI kits),acq.backends/common.sh(acq_cli_kits_write/acq_cli_kits_loadpersistence),acq.backends/sbx.sh(parity write), and theacqstart/restart/runverbs (reload before heal). Covered by theclikit-healandcli-kits:*unit tests intest/bats/(persist-then-reload round-trip; a reloaded--kitref is re-applied during a resume heal). - Note the operational fact behind the original report: a live in-VM session does not survive a host reboot — the microVM is ephemeral; only the sandbox definition, its port config, and your mounted repos persist. Always commit work to the mounted repos before shutting down, and expect to start a fresh agent session (not reattach the old one) after a reboot.
34. Git signing fails on msb: socat missing or msb < 0.6.9
Symptoms
On the msb backend, committing inside the guest fails to sign:
[git-ssh-sign] no SSH agent - cannot sign commits
or acq printed a warning at create/attach about socat not being found in the
guest image, or a "host ssh-agent forwarding needs msb >= 0.6.9" warning and
then skipped the forward.
Root Cause
acq forwards the host ssh-agent into an msb guest with
--vsock $SSH_AUTH_SOCK:3552/stream and then runs an in-guest socat bridge
that re-exposes the vsock route as the unix socket
/home/agent/.acq/ssh-agent.sock (exported as SSH_AUTH_SOCK). Two things must
hold for that to work:
- msb >= 0.6.9 —
--vsockfirst appears in msb 0.6.9. On an older msb, acq warns and skips the forward (fail-soft), so the guest has no agent. socatpresent in the guest image — the bridge issocat. It is not auto-installed (guest egress is locked, so a package mirror is unreachable); acq warns if it is missing and the bridge cannot start.
The forward is also opt-in via the host SSH_AUTH_SOCK: if no agent is
running on the host (or no key is loaded), there is nothing to forward.
Fix
-
Upgrade msb to >= 0.6.9 (
msb self update). -
Ensure
socatis inACQ_MSB_IMAGE— the defaultdocker/sandbox-templates:shell-dockerships it; a custom override must too. -
Ensure the host has an agent with a key loaded before running
acq:eval "$(ssh-agent -s)" # if none is running ssh-add ~/.ssh/id_ed25519 # load your signing key echo "$SSH_AUTH_SOCK" # must be set — this is the opt-in signal
Then re-run acq run …; the forward is applied automatically. See
ADR-0021 for the full
mechanism and the trust-boundary discussion.
Variant: SSH_AUTH_SOCK unset on re-attach to a running sandbox
A distinct signature of the same feature: signing works right after the initial
acq run but fails on a later re-attach to the same, still-running
sandbox, and it starts working again as soon as you
export SSH_AUTH_SOCK=/home/agent/.acq/ssh-agent.sock by hand. Here the vsock
route and the socat bridge are fine — what is missing is the
SSH_AUTH_SOCK env var in the agent's process. acq injects that var
(-e SSH_AUTH_SOCK=…) only when the persisted /var/lib/acq/ssh-auth-sock
marker is present, and before the fix nothing re-established the bridge or wrote
that marker when re-attaching to an already-running sandbox (the heal's
start-if-stopped block is a no-op on a running sandbox, and only that path — or
provision — wrote the marker).
Fixed in acq.backends/msb.sh: acq_backend_ensure_kits_applied now calls
_acq_msb_ensure_ssh_agent_forward at the top of the heal, which re-drives the
forward (restart the bridge + write the marker) on every re-attach when both a
host forward is requested (SSH_AUTH_SOCK set) and the sandbox carries the
create-time --vsock route. The route is create-time only, so a sandbox created
without a host agent still needs a recreate to gain forwarding — but one
created with the route now re-injects SSH_AUTH_SOCK on every re-attach without
a manual export. See ADR-0021
("Re-attach to a running sandbox").
Variant: forwarded agent unreachable after a host reboot (stale --vsock route)
Another distinct signature, and the one that a manual
export SSH_AUTH_SOCK=… does not fix: signing worked before, then after the
host machine was rebooted (especially an unclean reboot that did not stop the
sandbox first) a resume + re-attach leaves the guest unable to reach the agent —
ssh-add -l inside the guest returns error fetching identities: communication with agent failed, and committing fails to sign — even though SSH_AUTH_SOCK is
set on both the host and in the guest, the host agent holds the key, and the
in-guest socat bridge is running. Probing the bridge by hand shows the tell:
$ socat -T2 - VSOCK-CONNECT:2:3552 </dev/null
E connect(…, cid:2 port:3552, …): Connection reset by peer
Connection reset by peer on the vsock connect means the host side of the
route has no live listener. The --vsock HOST_PATH:3552 route is emitted
only at create time and pins HOST_PATH to the host's SSH_AUTH_SOCK path
captured then; it is persisted in the sandbox config and never re-derived on
msb start or re-attach. A host reboot restarts the host ssh-agent under a new
socket path, so the persisted route now points at a dead host endpoint. This is
distinct from the previous variant: there the guest env var was missing; here the
env var and bridge are present but the route's host end is dead.
acq now detects and reports this instead of leaving a silent dead bridge:
after (re)starting the bridge, _acq_msb_start_ssh_agent_bridge runs a liveness
probe (ssh-add -l over the guest sock) and, on failure, prints an actionable
warning naming the host-reboot cause and the remedy. Because the bridge's
listener socket is present (only its vsock backend is dead), ssh-add exits 1
with communication with agent failed here — the same exit code as a healthy but
empty agent, so the probe classifies on the message (a "no identities" reply is
treated as healthy and stays quiet). The --vsock route is create-time only, so
the fix is to recreate the sandbox to refresh the route:
acq rm <sandbox>
# then re-run your original create/run WITH the host agent available:
eval "$(ssh-agent -s)"; ssh-add ~/.ssh/id_ed25519 # if needed
acq run <agent> <workspace…>
See ADR-0021 ("Re-attach to a running sandbox") for the full mechanism.
35. Re-attach Heal Loop Warns Every Time on sbx 0.38 — sbx kit add Refuses Startup-Bearing Kits
Symptoms
On the sbx backend (sbx >= 0.38), every re-attach to an existing sandbox
(acq run <name>) reports the sandbox is "missing the playbook kit", re-attempts
every extra kit, and each sbx kit add warn-fails — on every attach, even
for a sandbox that was provisioned correctly. Running the printed recover command
by hand shows the real cause:
$ sbx kit add <sandbox> <translated-playbook-kit-dir>
ERROR: kit "agentic-coding-playbook" declares setup.startup, which the kit-add
recreate flow does not yet apply; recreate the sandbox from scratch via
`sbx rm` + `sbx create --kit` to use this kit
Root Cause
Three issues compounded:
- sbx 0.38 refuses startup-bearing kits mid-life.
sbx kit addon 0.38 only applies mixin kits declaring exclusivelyenvironment.variables,setup.install, andpermissions.network.allow(Docker kits docs). Every built-in acq kit declares startup commands (translated intosetup.startup), so all mid-life built-in kit adds are refused. acq swallowed sbx's stderr (sbx kit add … >/dev/null 2>&1) and printed a generic warning plus a "Recover with: sbx kit add …" hint that could never succeed. - A stale playbook feature-probe. The heal probed the playbook with
test -e "$HOME/.agentic-coding-playbook/.git". Since patterns v1.8.0 moved playbook delivery from a git clone to a REST tarball (the #203 fix), the tree has no.git, so the probe reported the playbook absent on every re-attach, forever — re-triggering the (now-refused) add each time. - The
~/.acq-extra-kitsmarker was never written at create. The re-attach heal decided whether an extra kit was already applied by reading~/.acq-extra-kits, but only the heal path wrote it. A sandbox created withACQ_EXTRA_KITS(or--kit) never got the marker, so every re-attach re-attempted every extra kit (and, under (1), warn-failed).
Fix
acq (this repo):
- Captures
sbx kit addstderr and detects the startup refusal, printing a single actionable "recreate to extend/refresh" message (acq rm <name> && acq run …) instead of per-kit warnings with recover commands that cannot work. Other (genuine) failures now surface sbx's own diagnostic rather than being hidden.acq kit updatefails with the same guidance. acq does not auto-recreate (a recreate discards the sandbox's session/context — it stays a deliberate, user-consented action). - Probes the playbook with
test -e "$HOME/.agentic-coding-playbook/AGENTS.md", which matches both the historical git-clone and the current tarball delivery. - Writes
~/.acq-extra-kitsat create for every extra/CLI kit, so re-attach correctly sees them as already applied.
To pick up a refreshed built-in bundle on sbx 0.38 you must recreate the sandbox (this discards its session/context):
acq rm <sandbox>
acq run opencode /path/to/your/project
msb is unaffected — it re-applies kits idempotently via msb exec and has no
sbx kit add. The upstream "not yet" in the sbx error suggests startup support in
the kit-add flow may return; if it does, in-place healing can be re-enabled. See
the update note in
ADR-0009.
Verifying the fix (live)
On a real sbx host (this cannot run inside a sandbox — no nested sandboxes), one command reproduces the exact scenario and asserts all three fixes:
./scripts/verify-issue-320
It provisions a throwaway sandbox with a startup-bearing extra kit, re-attaches,
and checks that: the extra-kit marker was written at create; the re-attach heal
is a quiet no-op (no "missing the playbook kit", no extra-kit re-attempt, no
bogus "Recover with" hint); a forced mid-life re-add surfaces exactly one
recreate notice; and acq kit update fails fast (or short-circuits when already
current). The offline regression guard is in test/bats/ (the sbx
heal-loop cases).
36. msb Create-Time Published Port Returns Empty Response for a Loopback-Only Guest Service
Symptoms
On the msb backend, acq ports <sandbox> shows a create-time published port,
and the service answers from inside the sandbox:
$ acq ports paseo-test
sandbox 6767 -> host 127.0.0.1:6767 (create-time -p)
$ acq exec paseo-test -- curl http://127.0.0.1:6767/
<!doctype html>
...
But the same port fails from the host:
$ curl http://127.0.0.1:6767/
curl: (52) Empty reply from server
Chrome shows (failed) net::ERR_EMPTY_RESPONSE. This is distinct from the older
ingress-deny failure (ERR_CONNECTION_RESET) in
ADR-0019: here the TCP connect
to the host listener succeeds, but the msb publisher cannot complete the
guest-side connection.
Root Cause
The service is listening only on guest loopback (127.0.0.1:GUEST). For
example, /proc/net/tcp shows 0100007F:1A6F, which is 127.0.0.1:6767.
msb create-time publishing (-p HOST:GUEST, which acq emits from neutral
publishedPorts) binds a listener on the host loopback by default, but the
publisher connects to the sandbox's guest network IP for GUEST; it does not
dial guest 127.0.0.1. A loopback-only service can therefore be healthy from
inside the sandbox while unreachable through create-time -p from the host.
Fix
Bind the guest service to 0.0.0.0:GUEST (or the guest interface address) when
you want it reachable through create-time publishedPorts / msb -p:
# Example shape; exact flag/env depends on the service
service --host 0.0.0.0 --port 6767
If the service must stay loopback-only, use acq's msb post-hoc publish path:
acq --backend msb ports <sandbox> --publish 16767:6767
curl http://127.0.0.1:16767/
That path uses msb ssh serve plus OpenSSH -L, so the connection originates
from inside the sandbox and can reach guest 127.0.0.1:6767.
Prevention / Status
- Documented in BACKEND_GUIDE.md under msb port limitations.
- When adding a kit that declares
publishedPorts, make its supervised service listen on0.0.0.0unless it intentionally requires loopback-only access and documents the post-hocacq ports --publishpath. - A quick discriminator is: if
acq exec <sandbox> -- curl http://127.0.0.1:GUESTworks but host curl returnsEmpty reply from server, inspect the guest listener bind address before chasing ingress policy or browser issues.
37. Commit signing fails in the sandbox: user.signingKey needs to be set / No signature
Symptoms
Inside the sandbox, git commit -S (or any commit while commit.gpgsign=true)
fails or silently produces an unsigned commit:
error: user.signingKey needs to be set for ssh signing
or the commit is created but git cat-file commit HEAD | grep '^gpgsig' finds
nothing, and git log --show-signature reports No signature. ssh-add -l
reports Could not open a connection to your authentication agent.
Root Cause
The sandbox reaches the host SSH signing key over a forwarded ssh-agent, but
SSH_AUTH_SOCK is not exported into every shell/session by default. With no
agent reachable, git's SSH signer has no key and either errors or (depending on
config) writes an unsigned commit.
This is environmental, not a git misconfiguration: user.signingkey,
gpg.format ssh, and commit.gpgsign true can all be correct and signing still
fails purely because the agent socket isn't wired into the current shell.
Fix
Export the forwarded agent socket for the commands that need to sign, then commit/rebase as usual:
export SSH_AUTH_SOCK=/home/agent/.acq/ssh-agent.sock
ssh-add -l # should now list the host signing key(s)
git commit -S ... # or: git rebase <base> (replayed commits are signed)
Verify the signature is embedded (this check needs no allowed_signers file):
git cat-file commit HEAD | grep -q '^gpgsig' && echo SIGNED || echo UNSIGNED
Notes:
git log --show-signaturemay still printgpg.ssh.allowedSignersFile needs to be configured and existand showNo signatureeven when the commit is signed — that is a local verification gap (no allowed-signers file), not a signing failure. GitHub verifies against its own registered-key store on push and will show Verified. Prefer thegit cat-file … | grep '^gpgsig'check to confirm signing.git rebasere-signs each replayed commit automatically when the agent is reachable, so a rebase withSSH_AUTH_SOCKset does not need a separate--amend -Spass.
38. Spurious M (modified) diffs on scripts across the host/sandbox mount
Symptoms
git status shows tracked files (commonly acq, scripts/test-acq,
scripts/verify-backends) as modified with no content change, and
git diff shows only mode lines:
old mode 100755
new mode 100644
The noise breaks scripted branch work: a git commit --amend picks up the mode
churn, leaving the working tree "dirty," so a following git switch <branch>
aborts with "Your local changes to the following files would be overwritten by
checkout" — and a loop that assumed the switch succeeded then operates on the
wrong branch.
Root Cause
The repository is worked on across a host↔sandbox boundary (the sandbox worktree
lives under a mounted volume). The host (macOS) and the sandbox (Linux) disagree
on the executable bit, so git sees the +x bit flip back and forth as
100755 ↔ 100644 filemode churn. It is not a real edit.
Fix
Tell git to ignore the executable bit in each working tree (the setting is per-working-tree, so setting it on the host does not cover the sandbox worktree and vice versa):
git config core.fileMode false # run in every clone/worktree of this repo
To make it the default for all future clones on a machine that routinely spans this mount:
git config --global core.fileMode false
Workflow guard: when scripting branch operations, do not loop git switch
across a possibly-dirty tree. Run one branch per command, and guard switches
(git switch "$b" || break) so a failed checkout cannot silently leave you
amending the wrong branch.
When something fails, work through this list:
- Is the secret actually in the container? (
echo $VAR_NAME) - Is the endpoint URL exactly correct? (no typos, correct path)
- Is the auth header format correct? (
Bearerprefix) - Can the container reach the network? (
curltest) - Is the config file actually being read? (add debug logging)
- Did SBX CLI syntax change? (
sbx --help) - Is this a known model/entitlement issue? (test with different model)
Contributing
When you discover a new failure mode:
- Document the symptoms clearly
- Identify the root cause
- Provide a minimal fix
- Add it to this document
- Consider if it indicates a gap in AGENTS.md rules