# Fishbowl
[](https://github.com/Antonlovesdnb/fishbowl/releases)
[](https://github.com/Antonlovesdnb/fishbowl/actions)
[](https://www.rust-lang.org/)
[]()
[](LICENSE)
[](AGENTS.md)
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:
1. **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](docs/credential-scanning.md) for the full list of paths and classification rules.

1. **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](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 via `security find-generic-password -w` into 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 `--mount` to avoid a malicious repo silently importing host secrets.

1. **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 which `openat()` events are interesting.
2. **Container launch.** Docker runs the agent inside a hardened container:
- `--cap-drop ALL --security-opt no-new-privileges`
- Project bind-mounted at `/` 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/`
3. **Monitoring starts.** Fishbowl picks the strongest monitoring available:
- **Linux:** host-side bpftrace via a `sudo` helper, 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, `ss` network polling)
4. **Agent runs.** Your agent does its work inside the container. Every `execve()`, `connect()`, and `openat()` on a monitored credential is captured.
5. **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 | sh
```
The script auto-detects your OS and architecture, downloads the right binary and the collector image from the latest [GitHub release](https://github.com/Antonlovesdnb/fishbowl/releases), 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.
### 卸载
```
curl -fsSL https://raw.githubusercontent.com/Antonlovesdnb/fishbowl/main/install.sh | sh -s -- --uninstall
```
Removes the binary, Docker images, and optionally `~/.fishbowl/` (prompts before deleting session data).
## 用法
```
# 运行当前目录
fishbowl run
# 运行特定项目
fishbowl run ~/projects/my-app
# 挂载凭据(自动检测类型:环境变量、SSH 密钥或凭据文件)
fishbowl run --mount GH_TOKEN --mount ~/.ssh/id_ed25519 --mount ~/secrets/service.json
# 使用主机网络(用于 VPN/实验室路由)
fishbowl run --network host
```
Mounted credentials appear inside the container at `/fishbowl/creds/` (credential files) and `/fishbowl/ssh/` (SSH keys). Environment variables are passed through directly.
## 查看会话
After a run, review what happened:
```
fishbowl audit # most recent session
fishbowl audit # specific session directory
```
The 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-/ # 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:
<_BLOCK_8/>
**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 load`s 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.jsonl` and `registry.json` to 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`, the `ss` network 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](https://github.com/Cybereason-Public/owLSM), [Falco](https://falco.org/), or [Tetragon](https://tetragon.io/) — Fishbowl is complementary to these tools, not a replacement for them.
## CI/CD 集成
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.
```
# 在 Fishbowl 中运行代理任务
fishbowl run ~/my-app --mount API_KEY -- codex "run the deploy script"
# 门控流水线 — 如果发生任何高严重性事件则失败
fishbowl check --fail-on high
```
Severity 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`/`python` reading credential files
- **Env var poisoning** — `PAGER`, `LD_PRELOAD`, `GIT_ASKPASS` mutations
- **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 high
```
If `fishbowl check` exits non-zero, the pipeline stops and the full session audit log is available for investigation.