Docker credentials¶
Coding agents store OAuth credentials in home-directory files (e.g.
~/.claude/.credentials.json). When you run an agent inside a Docker container, token
refresh creates new tokens (OAuth rotation) and invalidates the host's tokens. caw auth
solves this without modifying host files: it bind-mounts the host credentials into the
container and runs an inotify-based guard that keeps the container's copy and the bind-mounted
host file in sync, both directions.
caw auth setup # snapshot configs, write mount manifest
caw auth status # token expiry, last modified, mount flags
docker run $(caw auth docker-flags) -v ./project:/work my-image
caw auth teardown # rm -rf ~/.caw/auth/ (host files untouched)
How it works¶
HOST: CONTAINER:
~/.claude/.credentials.json ←—docker bind—→ /tmp/caw_auth/claude/credentials.json
(untouched, real file) ↓ copy + inotify sync
/home/playground/.claude/.credentials.json
~/.caw/auth/ only holds things caw legitimately owns: the manifest, the container setup
script, and cleaned/stripped configs. Credentials stay at their original host paths.
Commands¶
caw auth setup¶
Reads credentials and configs from the host, validates them, writes cleaned configs and a
credential snapshot into ~/.caw/auth/, and generates manifest.json + setup-container.sh.
Host credential files are read but never modified.
- Credential files (tokens, OAuth) —
strategy: bind. Bind-mounted from the host into the container at run time; the container-side guard copies them to the user's home and keeps them in sync. - Config files (
.claude.json,config.toml) —strategy: copy. Cleaned/stripped for containers and shipped in the staging directory.
caw auth status¶
Shows a table with each managed file, where its source of truth lives (host for bind, staged for copy), last-modified time, and token expiry for credential files. Credential freshness is read from the host file directly.
caw auth docker-flags¶
Emits one directory mount for the staging area plus one file mount per credential:
$ caw auth docker-flags
-v /home/user/.caw/auth:/tmp/caw_auth:rw \
-v /home/user/.claude/.credentials.json:/tmp/caw_auth/claude/credentials.json:rw \
-v /home/user/.codex/auth.json:/tmp/caw_auth/codex/auth.json:rw
Command substitution ($(caw auth docker-flags)) expands these into separate docker run
arguments.
caw auth teardown¶
Removes ~/.caw/auth/. Host credential files are never involved. It refuses to run if a host
credential file is still a symlink into the auth dir (leftover from caw's old symlink-based
design); pass --force to override — but you'll have to re-authenticate every agent.
Container setup¶
The generated setup-container.sh runs inside the container (called from your entrypoint). It
reads manifest.json, copies credentials and configs into the container user's home, and
starts a bidirectional inotify guard for credential sync.
# In your entrypoint.sh:
if [ -f /tmp/caw_auth/setup-container.sh ]; then
/tmp/caw_auth/setup-container.sh /tmp/caw_auth /home/playground playground
fi
The guard runs as root and uses plain cp (no --preserve, no chown on the mount side), so
writes back to the host file preserve the host user's uid/gid/mode on the real inode. Requires
jq in the container image; inotify-tools is installed automatically if not present.
Supported agents¶
| Agent | Credential files | Config files |
|---|---|---|
| Claude Code | .claude/.credentials.json |
.claude.json (stripped to essential keys) |
| Codex | .codex/auth.json |
.codex/config.toml (local trust removed) |
Programmatic API¶
The same operations are available in Python:
from caw.auth import setup, teardown, get_status, get_docker_flags
setup(agents=["claude"])
statuses = get_status()
flags = get_docker_flags()
teardown()
These are also re-exported at the top level as
auth_setup, auth_get_status, and
auth_get_docker_flags. Full example —
examples/auth.py:
"""Programmatic auth: set up credentials, check status, and get docker flags."""
from pathlib import Path
from caw.auth import setup, get_docker_flags, get_status
def main():
# Set up credentials to a custom directory (instead of ~/.caw/auth)
auth_dir = Path("./my_project_auth")
print("=== Setting up credentials ===")
setup(agents=["all"], dest_dir=auth_dir)
# Check status of all collected auth files
print("\n=== Auth file status ===")
statuses = get_status(auth_dir=auth_dir)
for s in statuses:
print(f" {s.agent}/{s.file}: type={s.type}, strategy={s.strategy}, exists={s.exists}")
if s.token_expiry:
print(f" token: {s.token_expiry}")
# Get docker volume flag for mounting auth into a container
print("\n=== Docker flags ===")
flags = get_docker_flags(auth_dir=auth_dir)
print(f" {flags}")
# Use it to construct a docker command
docker_cmd = f"docker run {flags} my-agent-image"
print(f"\n Full command: {docker_cmd}")
if __name__ == "__main__":
main()
See the Auth API reference for full signatures.
Known limitations¶
- OAuth token rotation. A refresh returns a new refresh token, invalidating the old one. If two processes refresh simultaneously, one gets an invalid token. Don't run the same agent identity in two places at once.
- Atomic rewrites. If an agent refreshes by writing a temp file and
rename(2)-ing it over the credential, a single-file bind mount detaches from the new inode. If that becomes a problem for a given agent, switch its bind to a directory bind (mount the parent directory), which survives renames.