Fix stale AWS/Bedrock auth carrying over on backend switch

When a project switched backends (e.g. Bedrock -> Anthropic), the
recreated container kept authenticating against Bedrock, and SSO kept
firing after switching away. Three root causes, all fixed:

1. Recreation builds the new container from a `docker commit` snapshot.
   commit always bakes the previous container's full ENV into the image
   (an empty commit Config does NOT strip it, and the commit API cannot
   remove env). So CLAUDE_CODE_USE_BEDROCK=1 / AWS_* survived into the
   new container. Fix: create_container now explicitly clears every
   managed auth key the active backend does not set (MANAGED_AUTH_KEYS),
   so create-time env overrides the stale baked-in values.

2. awsAuthRefresh was written into ~/.claude.json (persisted home
   volume) and never removed, so Claude Code kept invoking
   triple-c-sso-refresh after switching to a non-SSO backend. Fix:
   entrypoint now deletes awsAuthRefresh when AWS_SSO_AUTH_REFRESH_CMD
   is unset, idempotent both ways.

3. Static/session AWS creds were baked into Config.Env at create time,
   so a stop/start kept stale creds and rotated keys never refreshed
   without a full recreation. Fix: static creds are no longer injected
   as env vars; write_bedrock_static_credentials() writes
   ~/.aws/credentials (0600, secrets via exec env not argv) on every
   start, and removes a stale ~/.aws/config left from a prior profile/SSO
   session. Static creds also dropped from the bedrock fingerprint so a
   key rotation refreshes in place instead of forcing recreation.

Adds exec_oneshot_env() for env-carrying one-shot execs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 13:34:43 -07:00
co-authored by Claude Opus 4.8
parent 1716fb5c82
commit 424ab04ca8
4 changed files with 167 additions and 17 deletions
+13
View File
@@ -281,6 +281,18 @@ impl ExecSessionManager {
/// Run a one-shot (non-interactive) exec command in a container and collect stdout.
pub async fn exec_oneshot(container_id: &str, cmd: Vec<String>) -> Result<String, String> {
exec_oneshot_env(container_id, cmd, Vec::new()).await
}
/// Like `exec_oneshot`, but passes additional environment variables to the exec
/// process. Secrets passed this way live only in `/proc/<pid>/environ` (readable
/// by the same user / root) rather than in the process argv, so they are not
/// exposed via `ps`.
pub async fn exec_oneshot_env(
container_id: &str,
cmd: Vec<String>,
env: Vec<String>,
) -> Result<String, String> {
let docker = get_docker()?;
let exec = docker
@@ -290,6 +302,7 @@ pub async fn exec_oneshot(container_id: &str, cmd: Vec<String>) -> Result<String
attach_stdout: Some(true),
attach_stderr: Some(true),
cmd: Some(cmd),
env: if env.is_empty() { None } else { Some(env) },
user: Some("claude".to_string()),
..Default::default()
},