A containerized credential auditing perimeter for AI coding agents. Validated with Codex and Claude Code on both macOS and Linux.
Fishbowl wraps your AI agent in a Docker container, audits every credential access, environment variable mutation, and outbound network connection, then gives you a session report. It's observation-only — it doesn't block or kill anything the agent does.
The container is the security boundary. The agent can see your project directory, its own auth files (auto-mounted copies of ~/.codex/ or ~/.claude/), any credentials you explicitly --mount, and the session logs — but not the rest of your home directory or system. The container filesystem is read-only, all Linux capabilities are dropped, and privilege escalation is disabled.
NOTE - fishbowl is a vibe coded project, please evaluate and test it before utilizing it in production use-cases
When you run fishbowl run ~/my-project, this is what happens:
-
Host credential scan. Fishbowl walks your home directory and project for known credential files (
.env,~/.aws/credentials,~/.codex/auth.json, SSH keys, etc.) and prints what it finds. The scan report is saved to a host-only location (~/.fishbowl/host-scans/) — it is NOT visible inside the container.See docs/credential-scanning.md for the full list of paths and classification rules.
-
Agent auto-detection. Based on project markers (
CLAUDE.md,AGENTS.md), host auth artifacts (~/.codex/,~/.claude/), and environment variable references, Fishbowl picks the agent type and auto-mounts the relevant auth files into/fishbowl/home/inside the container.See docs/agent-detection.md for the detection priority cascade and what each agent gets. On macOS, Claude Code stores its OAuth token in the login Keychain under service
"Claude Code-credentials"rather than in~/.claude/.credentials.json; Fishbowl extracts it viasecurity find-generic-password -winto the per-session runtime auth dir (0o600, parent 0o700, cleaned up with the rest of the session) so Claude running inside the Linux container can read it as a regular file. First run may trigger the standard macOS "allow security to access your keychain" dialog. Credential env vars and SSH keys referenced in project text are NOT auto-passed — Fishbowl prints them as recommendations but requires explicit--mountto avoid a malicious repo silently importing host secrets.
-
Registry seeding. Credential paths from the host scan are translated to their in-container equivalents and written to the runtime credential registry (
registry.json). This is how the file collector knows whichopenat()events are interesting. -
Container launch. Docker runs the agent inside a hardened container:
--cap-drop ALL --security-opt no-new-privileges- Project bind-mounted at
/<project-name>and/workspace - Selected credentials at
/fishbowl/creds/and/fishbowl/ssh/(read-only) - Agent auth at
/fishbowl/home/(the container's$HOME) - Session logs at
/var/log/fishbowl/
-
Monitoring starts. Fishbowl picks the strongest monitoring available:
- Linux: host-side bpftrace via a
sudohelper, scoped to the container's cgroup - macOS: bpftrace in a privileged sidecar container inside the Docker VM (auto-detects Docker Desktop, Colima, OrbStack, Rancher Desktop)
- Fallback: if the strong path fails (no root, Docker not running, collector image missing), prints the reason and continues with container-local telemetry (bash env hooks, inotify file watchers,
ssnetwork polling)
- Linux: host-side bpftrace via a
-
Agent runs. Your agent does its work inside the container. Every
execve(),connect(), andopenat()on a monitored credential is captured. -
Shutdown. When the agent exits, Fishbowl gracefully drains the bpftrace collectors (SIGINT + 1.5s grace period) and tears down the helper container. Session state is synced back to the host for Codex/Claude.
curl -fsSL https://raw.githubusercontent.com/Antonlovesdnb/fishbowl/main/install.sh | shThe script auto-detects your OS and architecture, downloads the right binary and the collector image from the latest GitHub release, verifies the SHA256 checksum, and installs to /usr/local/bin (or ~/.local/bin if no write access).
Supported platforms: macOS (Apple Silicon) and Linux (x86_64 + arm64). Linux binaries are fully static (musl libc) so they run on any distro including Alpine.
Requirements: a container runtime — Docker Desktop, Colima, OrbStack, or Rancher Desktop — must be running before fishbowl run.
Options: pin a version with FISHBOWL_VERSION=v2.1.1, override the install directory with FISHBOWL_BIN_DIR=....
That's the whole install. The container image gets built automatically the first time you run fishbowl run (a few minutes; one-time). If you'd rather get that out of the way up front, run fishbowl build-image after installing.
Building from source:
cargo install --path .(requires Rust >= 1.85). Only needed if you're contributing or want to modify the container image. The firstfishbowl runwill build the container image automatically, same as the prebuilt-binary path.
curl -fsSL https://raw.githubusercontent.com/Antonlovesdnb/fishbowl/main/install.sh | sh -s -- --uninstallRemoves the binary, Docker images, and optionally ~/.fishbowl/ (prompts before deleting session data).
# Run the current directory
fishbowl run
# Run a specific project
fishbowl run ~/projects/my-app
# Mount a credential (auto-detects type: env var, SSH key, or credential file)
fishbowl run --mount GH_TOKEN --mount ~/.ssh/id_ed25519 --mount ~/secrets/service.json
# Use host networking (for VPN/lab routes)
fishbowl run --network hostMounted credentials appear inside the container at /fishbowl/creds/<filename> (credential files) and /fishbowl/ssh/<filename> (SSH keys). Environment variables are passed through directly.
After a run, review what happened:
fishbowl audit # most recent session
fishbowl audit <SESSION> # specific session directoryThe audit report shows:
- Credentials — each discovered credential, its classification, access count, and expected destinations
- Alerts — medium/high/critical severity events (env mutations, credential access by suspicious processes)
- Network — outbound destinations with connection counts and alert flags
Note - fishbowl audit is meant to be run outside of the container.
All session data lives under ~/.fishbowl/logs/:
~/.fishbowl/
logs/
latest -> session-1775780487 # symlink to most recent
session-1775780487/ # one directory per run
audit.jsonl # all audit events (JSONL)
registry.json # credential registry (live state)
findings.jsonl # credential-egress correlation findings
ebpf_exec.jsonl # host eBPF: process exec events
ebpf_connect.jsonl # host eBPF: network connect events
ebpf_file.jsonl # host eBPF: credential file access events
ebpf_scope.json # eBPF container scope metadata
ebpf_*.stderr.log # bpftrace stderr (empty = probes attached OK)
host-scans/
session-1775780487.json # host credential path enumeration (host-only)
runtime/
session-1775780487-<nonce>/ # runtime auth copies (cleaned up after 6h)
audit.jsonl — one JSON object per line, every event from both in-container watchers and host eBPF collectors:
{
"timestamp": "2026-04-10T00:21:29+00:00",
"event": "process_exec",
"severity": "info",
"agent": "host-ebpf",
"command": "/bin/cat",
"path": "/usr/bin/cat",
"process_name": "cat",
"observed_pid": "40643",
"process_chain": "cat(pid=40643) <- bash(pid=40626) <- tini <- containerd-shim <- systemd",
"env_findings": [{"variable": "BASH_ENV", "value_preview": "/age...(redacted,len=23)"}],
"discovery_method": "host_ebpf_exec",
"verdict": "observed"
}Event types: process_exec, env_mutation, env_enumeration, credential_discovery, credential_access, network_egress, workspace_credential_access.
Full credential values are not intentionally logged. Environment variable findings include a short preview (first 4 characters + length) for classification purposes — e.g. sk-p...(redacted,len=48). Credential env vars are passed to Docker via --env-file (not CLI args) to avoid exposure in the host process table.
registry.json — live credential registry, updated as credentials are discovered and accessed:
{
"credentials": [
{
"id": "file::/fishbowl-smoke/.env",
"classification": "Project .env Credential File",
"discovery_method": "project_scan",
"path": "/fishbowl-smoke/.env",
"access_count": 3,
"last_accessed_at": "2026-04-10T00:21:29+00:00"
}
]
}ebpf_file.jsonl — credential access events from the kernel file collector:
{
"event": "credential_access",
"process_name": "cat",
"raw_path": "/workspace/.env",
"resolved_path": "/fishbowl-demo/.env",
"operation": "openat",
"classification": "Project .env Credential File",
"process_chain": "cat <- bash <- tini <- containerd-shim <- systemd",
"collector": "bpftrace_file"
}ebpf_exec.jsonl — every process spawn inside the container:
{
"event": "process_exec",
"process_name": "bash",
"filename": "/usr/bin/curl",
"cmdline": "curl -sS https://example.com/",
"process_chain": "curl <- bash <- tini <- containerd-shim <- systemd",
"env_findings": [
{
"variable": "BASH_ENV",
"classification": "Dangerous Execution Environment Variable",
"value_preview": "/age...(redacted,len=23)"
}
],
"collector": "bpftrace_exec"
}audit.jsonl — env mutations caught by the bash hooks:
{
"event": "dangerous_env_mutation",
"severity": "medium",
"command": "export PAGER=\"evil-pager\"",
"variable": "PAGER",
"new_value": "\"evi...(redacted,len=12)",
"reason": "dangerous variable mutation command observed"
}findings.jsonl — credential-access-then-network-connect correlation findings (e.g., "process read ~/.codex/auth.json then connected to 185.x.x.x:443").
| Platform | Monitoring | Notes |
|---|---|---|
| Linux (source or binary) | Host-side eBPF via sudo helper |
Full exec/connect/file coverage, cgroup-scoped. No collector image needed — bpftrace runs as the host binary. |
| macOS (source or binary) | eBPF sidecar in Docker VM | Full coverage. install.sh downloads the pre-built collector image from the release and docker loads it; source installs build it via fishbowl build-image. Auto-detects Docker Desktop/Colima/OrbStack/Rancher. |
| Any host, fallback | Container-local watchers | If the eBPF path fails (no root on Linux, Docker not running, etc.), Fishbowl falls back to bash env hooks, inotify file watchers, and ss network polling. |
Container images are platform-specific. After cloning to a different architecture, run fishbowl build-image before fishbowl run.
-
In-container audit log is writable by the agent. The in-container watchers write
audit.jsonlandregistry.jsonto a writable subdirectory (/var/log/fishbowl/watcher/) inside the container. A compromised agent could tamper with this watcher output. However, the host-side eBPF logs (ebpf_*.jsonl) are protected — the parent session logs directory is mounted read-only into the agent container, and the eBPF logs are written by the helper container via its own mount. So the high-fidelity event data (exec, connect, file access from the kernel layer) is tamper-proof; only the in-container watcher events are at risk. -
Fallback monitoring has coverage gaps. When strong monitoring (the default) is unavailable — no root on Linux, or collector image missing on macOS — Fishbowl falls back to container-local watchers. These have known gaps: bash env hooks don't fire for
sh/python/node, thessnetwork poller misses sub-50ms connections, and UDP/DNS isn't covered. Strong monitoring covers all of these via kernel-level eBPF probes. -
Tested agents. Only Codex and Claude Code have been validated end-to-end. Cursor, Windsurf, and Copilot have scaffolded enum variants in the code but the wrapped-session flow hasn't been exercised for them.
Fishbowl provides visibility into opportunistic credential exfiltration — malicious npm/pip postinstall scripts, env-var poisoning (CVE-2026-22708), MCP config tampering via prompt injection (CVE-2025-54135/54136), and prompt injection that runs curl/wget to exfiltrate credentials.
Out of scope: determined adversaries who specifically target the monitoring stack, the agent encoding credentials into its own API channel (e.g. to api.anthropic.com), and sophisticated multi-step exfil chains.
Fishbowl is observation-only at runtime. It does not block, terminate, or interfere with the agent. For kernel-level prevention with enforcement, see owLSM, Falco, or Tetragon — Fishbowl is complementary to these tools, not a replacement for them.
Use fishbowl check to gate CI/CD pipelines on session security. It reads the session logs, counts events by severity, and exits non-zero if the threshold is exceeded.
# Run the agent task inside Fishbowl
fishbowl run ~/my-app --mount API_KEY -- codex "run the deploy script"
# Gate the pipeline — fail if any high-severity events occurred
fishbowl check --fail-on highSeverity levels: low, medium, high, critical. The default threshold is high.
Fishbowl Check
Session: /Users/dev/.fishbowl/logs/session-1775954089
Threshold: --fail-on high
Events: 12 total (2 info, 0 low, 9 medium, 1 high, 0 critical)
eBPF: 9 exec, 2 file, 1 connect
Result: FAIL (1 events at or above high severity)
HIGH credential_access_by_network_tool: curl accessed credential file /workspace/.env
What it catches:
- Credential exfiltration —
curl/wget/pythonreading credential files - Env var poisoning —
PAGER,LD_PRELOAD,GIT_ASKPASSmutations - Supply chain attacks — malicious postinstall scripts accessing secrets
- MCP config tampering — unauthorized server additions during agent sessions
- Prompt injection — agent tricked into running exfiltration commands
In a GitHub Actions workflow:
- name: Deploy with Fishbowl
run: |
fishbowl run . --mount DEPLOY_KEY -- codex "deploy to staging"
fishbowl check --fail-on highIf fishbowl check exits non-zero, the pipeline stops and the full session audit log is available for investigation.

