Skip to content

JAVA_TOOL_OPTIONS trust-store export is invisible to non-interactive shells (agent-spawned JVMs fail PKIX) #102

Description

@shejnowicz

Found while verifying #18 hands-on (2026-08-19, real container, --stacks java).

Problem

For devbox-installed JDKs, sandcat points every JVM at the mitmproxy-aware trust store via an env export appended to the tail of ~/.bashrc (baked in Dockerfile.app):

[ -L "$HOME/.local/share/sandcat/java-home" ] && export JAVA_HOME=...
[ -f "$HOME/.local/share/sandcat/cacerts" ] && export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=... -Djavax.net.ssl.trustStorePassword=changeit"

Debian's stock ~/.bashrc early-returns for non-interactive shells (case $- in *i*) ;; *) return;; esac), so anything appended after that guard is only visible in interactive shells.

Reproduction (fresh sandcat init --agent claude --stacks java project)

$ docker compose exec -T -u vscode agent bash -ic 'echo $JAVA_TOOL_OPTIONS; java /tmp/Fetch.java https://github.com'
-Djavax.net.ssl.trustStore=/home/vscode/.local/share/sandcat/cacerts -Djavax.net.ssl.trustStorePassword=changeit
HTTP 200                        # interactive: works

$ docker compose exec -T -u vscode agent bash -lc 'echo $JAVA_TOOL_OPTIONS; java /tmp/Fetch.java https://github.com'
                                # (empty)
javax.net.ssl.SSLHandshakeException: PKIX path building failed ...   # non-interactive login shell: FAILS

(Fetch.java = 10-line java.net.http.HttpClient GET, single-file source mode.)

Why it matters

Interactive terminals — the primary human workflow — are fine. But non-interactive contexts miss the trust store: scripts run via bash -lc, CI-style invocations inside the sandbox, and potentially JVM commands spawned by the AI agent itself (agent tool-calls typically execute non-interactive shells). Result: Maven/Gradle/java invoked non-interactively fails PKIX against the transparent proxy even though the trust store copy exists and contains the CA.

Same failure class as the SSL_CERT_FILE-only-in-login-shells issue that #91 fixed for uv (by moving the knob to compose-level environment).

Suggested fix

Move the two exports out of .bashrc-tail into a scope that all shells (and the agent process tree) inherit — candidates, roughly in order of robustness:

  1. sandcat.env (sourced by app-init.sh into the agent process env and copied to /etc/profile.d/ — covers login shells AND the agent's inherited environment), or
  2. a dedicated /etc/profile.d/sandcat-java.sh written by app-user-init.sh after it prepares the cacerts copy, or
  3. compose-level services.agent.environment (like Set uv TLS env vars automatically for the python stack #91 did for UV_SYSTEM_CERTS) — though the trust-store path is only valid once app-user-init.sh has created the copy, so a start-time-written profile.d file may fit better than a static compose entry.

The guards ([ -L ... ], [ -f ... ]) should move with the exports.

Notes

  • The apt-JDK path is unaffectedca-certificates-java's hook regenerates /etc/ssl/certs/java/cacerts from the system store and the Debian JDK symlinks its own cacerts to it, so no env var is involved (verified in the same session: HTTP 200 with JAVA_TOOL_OPTIONS explicitly unset).
  • scala-cli's dedicated config (~/.local/share/scalacli/secrets/config.json) is also unaffected — it doesn't rely on the env var.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions