diff --git a/.gitignore b/.gitignore index f344159..67cb259 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ .DS_Store Thumbs.db +# Railguard per-session traces and snapshots +.railguard/ + linux-server/adguard/work/ linux-server/adguard/conf/ diff --git a/agentic-ai/Claude/CLAUDE.md b/agentic-ai/Claude/CLAUDE.md new file mode 100644 index 0000000..ce19ff8 --- /dev/null +++ b/agentic-ai/Claude/CLAUDE.md @@ -0,0 +1,3 @@ +@rules/common/general.md +@rules/common/agents.md +@rules/bash/style.md diff --git a/agentic-ai/Claude/PLAN.md b/agentic-ai/Claude/PLAN.md new file mode 100644 index 0000000..1adde87 --- /dev/null +++ b/agentic-ai/Claude/PLAN.md @@ -0,0 +1,144 @@ +# Claude Code Config — Roadmap + +Tracks future improvements to `agentic-ai/Claude/`. Current state: `bypassPermissions` + OS sandbox + PreToolUse/PostToolUse/Stop hooks. + +--- + +## Done (in this branch) + +- `bypassPermissions` + `validate-bash.sh` / `validate-write.sh` PreToolUse hooks +- `post-edit-shellcheck.sh` PostToolUse hook +- `driftcheck.sh` Stop hook +- Hierarchical rules (`rules/common/`, `rules/bash/`) +- OS-level sandbox via bubblewrap (`setup-linux-sandbox.sh`) +- GPG signing + `gh api` enabled: `~/.gnupg` and `~/.config/gh` in `allowWrite`, not in `denyRead` +- Hook fixes: `set -euo pipefail` + `trap 'exit 2' ERR` on all hook scripts +- Fix `cat | jq` anti-pattern in `validate-bash.sh` +- Fix `read` on empty files in `driftcheck.sh` +- README corrected: accurate `denyRead` docs, security tradeoffs section added +- **Disabled bwrap sandbox** (`sandbox.enabled: false`): seccomp BPF unconditionally blocks all `AF_UNIX` socket calls on Linux (upstream issue [#44180](https://github.com/anthropics/claude-code/issues/44180), no fix timeline). This breaks GPG commit signing and SSH agent — both required for multi-VCS workflows with signed commits. Hooks remain the primary safety layer; `denyRead`/`allowWrite` config is preserved for re-enablement when #44180 is resolved. +- **Per-device SSH/GPG keys**: keys are generated per machine, not stored in a password manager. Passphrases (if any) may be stored in a vault, but key material stays on-device. +- **`denyWrite` entries added**: `/etc`, `/usr`, `/boot`, `/sys`, `/proc` in sandbox `filesystem.denyWrite`. Staged for when #44180 ships; has no effect while `sandbox.enabled: false`. +- **GH_TOKEN dropped**: provider-agnostic workflow (GitHub, Bitbucket, Forgejo) requires GPG signing and SSH agent for all remotes. A GitHub-only PAT is not a viable path. `~/.config/gh` remains in `allowWrite`; auth is handled via SSH keys per provider. +- **Railguard v0.5.1 integrated** (2026-05-22): hook layer adopted, railguard-shell skipped. Custom blocks for `sudo` escalation and `git add -A/--all/.`; `~/.gnupg` and `~/.config/gh` excluded from `denied_paths`. `CLAUDE_CODE_SHELL` intentionally unset to avoid bwrap conflict (#44180). See item 2 below for full evaluation and integration notes. +- **Heredoc false-positive fixed** (2026-05-22): `validate-bash.sh` was matching `sudo` and `git add` patterns against the full command string, including heredoc bodies (e.g. commit message text). Fixed by scoping those two policy checks to `FIRST_LINE`; destructive-pattern checks keep full-string matching. Duplicate custom blocks removed from `railguard.yaml` — those patterns used the same full-string regex and would have re-introduced the false positive. `validate-bash.sh` now owns the escalation/bulk-staging rules with correct scoping; railguard retains its built-in detection layer (encoding, path fence, memory guard, audit trail). +- **Railguard observed behaviors** (2026-05-22): Two self-protection behaviors surfaced in practice: (1) editing `railguard.yaml` triggers rule `railguard-config-edit-2` and requires human approval — intentional, prevents Claude from weakening its own guardrails; (2) Tier 3 behavioral evasion detection correlated all subsequent `git` commands with an earlier blocked `git commit`, requiring repeated human approval. Known false-positive; no fix available from within a session. +- **`validate-bash.sh` regex hardening** (2026-05-22): piped interpreter execution (`curl/wget | python/ruby/node/perl`), inline interpreter exec (two-grep AND checks for `python3? -c`+`os.system/subprocess`, `node -e`+`child_process/execSync`, `perl -e`+`system(/exec(`), all scoped to `FIRST_LINE` (same heredoc-safety rule as `sudo`/`git add`). +- **PostToolUse test runner hook** (2026-05-22): `post-test-runner.sh` — runs after every Write/Edit on source files; skips non-source extensions; discovers test command via `.claude/test-cmd` override or auto-detect (`cargo test`, `go test ./...`, `pytest`, `npm test`, `make test`); 60s timeout; warns on failure, silent on pass. + +--- + +## Near-term + +### 1. Re-enable sandbox when upstream fixes AF_UNIX blocking + +The bwrap sandbox adds meaningful defense-in-depth: kernel-level filesystem isolation that hooks cannot replicate. Worth re-enabling when feasible. + +**Blocker:** [#44180](https://github.com/anthropics/claude-code/issues/44180) — seccomp BPF unconditionally blocks AF_UNIX sockets on Linux. No config workaround. Blocks `gpg-agent` (commit signing) and `ssh-agent` (SSH push) — both required for a provider-agnostic workflow across GitHub, Bitbucket, and Forgejo. A GitHub-only PAT is not a viable substitute. + +**When #44180 ships:** flip `sandbox.enabled` to `true`. The `denyRead`/`allowWrite` filesystem config is already in place. Run `git commit --allow-empty -S` and `git push` to verify GPG signing and SSH agent both work before closing. + +**Evaluated alternatives (both ruled out for personal dev workflows):** + +- **Docker sandbox** ([docs](https://docs.docker.com/ai/sandboxes/agents/claude-code/)) — containerized Claude Code; credentials live entirely outside the container via OAuth proxy. Designed for untrusted-code execution. No SSH agent or GPG forwarding supported by design. +- **NVIDIA AI Workbench** ([docs](https://docs.nvidia.com/ai-workbench/user-guide/latest/quickstart/quickstart-claude-sandbox.html)) — bwrap + socat dual-layer isolation; `enableWeakerNestedSandbox: true` for running inside Docker (avoids nested namespace issues). Still blocks credential access. Relevant if Claude Code ever runs inside a container on this setup. + +Both approaches trade credential access for stronger isolation — appropriate for CI/CD or multi-tenant setups, not personal dev machines requiring GPG signing over SSH remotes. + +### 2. Railguard evaluation — COMPLETE (paper evaluation, 2026-05-22) + +[Railguard](https://github.com/railyard-dev/railguard) is a per-command policy engine that intercepts every Claude tool call. Source reviewed at v0.4.0. No live install performed — see integration conflicts below. + +#### What railguard adds over our current hooks + +| Capability | Our hooks | Railguard | +|---|---|---| +| Block destructive bash patterns | `validate-bash.sh` regex | Policy engine: allow/block/approve per named rule | +| Block writes to sensitive paths | `validate-write.sh` | Path fence: extracts paths from commands (handles pipes, subshells) | +| Encoding/obfuscation detection | Not covered | Tier 1: catches `base64 -d \| sh`, `eval`, `chr()`, inline exec patterns | +| Behavioral evasion detection | Not covered | Tier 3: detects re-attempt of previously blocked command with different syntax | +| Memory write classification | Not covered | Memory guard: blocks API keys in memory, requires approval for behavioral instruction injection | +| Per-edit rollback | Not covered | Snapshot every Write/Edit; `railguard rollback --steps N` | +| Audit trail | Not covered | `.railguard/traces/` structured log per session | +| Session termination | Not covered | Terminates session on repeated high-threat patterns; requires human approval to resume | +| Self-protection | Not covered | Blocks writes to `~/.claude/settings.json` and railguard binary | +| OS-level sandbox | Disabled (see #44180) | railguard-shell (see Linux conflict below) | + +Railguard is strictly better at the hook layer. `validate-bash.sh` + `validate-write.sh` would be replaceable. + +#### Linux OS sandbox conflict (same outcome as #44180) + +railguard-shell is a separate binary Claude Code runs as its shell via `CLAUDE_CODE_SHELL`. On Linux ≥ 5.13 (this machine: kernel 6.17), `detect_sandbox()` returns `LinuxLandlock` and `exec_linux_sandbox` wraps every Bash command in bwrap with: + +``` +--tmpfs /tmp # wipes SSH agent sockets (/tmp/ssh-*/agent.*) +--tmpfs ~/.gnupg # wipes GPG keyring +# /run not mounted # GPG agent socket (/run/user/$UID/gnupg/) unreachable +``` + +This breaks GPG commit signing and SSH push — same workflow impact as the disabled built-in sandbox (#44180), via filesystem namespacing instead of seccomp BPF. **railguard-shell is a non-starter on this machine** until the credential access problem is solved (GPG and SSH agent sockets need to be accessible inside the sandbox). + +The hook-based detection layer (threat classifier, memory guard, policy engine) is independent of railguard-shell and works without it. + +#### Installation conflicts with this repo's structure + +`railguard install` writes directly to `~/.claude/settings.json`, which in this setup is a **symlink to the repo**. Three problems: + +1. **Hook overwrite**: installs only railguard's three hooks (PreToolUse, PostToolUse, SessionStart), replacing `post-edit-shellcheck` and `driftcheck`. Those would need re-adding. +2. **`CLAUDE_CODE_SHELL` env**: writes the railguard-shell path into `settings.json` — activates the broken Linux sandbox automatically. +3. **CLAUDE.md injection**: appends ` ... ` to `~/.claude/CLAUDE.md`, which is also a symlink to the repo. + +All three write through symlinks into tracked repo files. An install would need to be followed by selective reverting. + +#### No pre-built binary for Linux + +v0.4.0 release has no Linux asset. Installation requires `cargo install railguard`. Rust toolchain not currently present. + +#### Status: INTEGRATED (2026-05-22) + +Hook layer integrated, railguard-shell skipped. + +What was done: +1. `rustup` installed non-interactively; `cargo install railguard` pulled v0.5.1 +2. `railguard.yaml` created at repo root with custom `sudo-escalation` and `git-add-bulk` blocks; `~/.gnupg` and `~/.config/gh` removed from `denied_paths` (required for GPG signing and gh CLI) +3. Railguard's three hooks (`PreToolUse`, `PostToolUse`, `SessionStart`) merged into `settings.json` alongside existing `validate-bash.sh`, `validate-write.sh`, `post-edit-shellcheck.sh`, and `driftcheck.sh` — railguard did NOT overwrite them +4. `CLAUDE_CODE_SHELL` env var intentionally NOT set — railguard-shell (bwrap) is only activated by that var; omitting it leaves the hooks active without triggering the Linux sandbox conflict +5. `install.sh` updated to symlink `railguard.yaml → ~/.railguard.yaml` (global policy for all `~/` projects via `find_policy_file` walk-up) +6. `.railguard/` added to `.gitignore` (per-session traces and snapshots) + +Revisit railguard-shell when either: (a) upstream #44180 ships and AF_UNIX is unblocked, or (b) railguard adds a `--share-path /run/user/$UID` option to its bwrap invocation. + +### 3. `enableWeakerNetworkIsolation` — no action (Linux no-op) + +Claude Code offers this flag to allow Go-based CLI tools (`gh`, etc.) to verify TLS certificates via `com.apple.trustd.agent` on macOS. On Linux, Go uses system CAs directly so this is a no-op for us. Leave unset. + +Reference: [Fixing gh CLI in Claude Code's sandbox](https://zencoder.ai/blog/fixing-gh-cli-in-claude-codes-sandbox) + +### 4. `enableWeakerNestedSandbox` for container deployments + +If Claude Code ever runs inside a Docker container, bwrap cannot create user namespaces (nested namespaces are blocked). NVIDIA's config adds `enableWeakerNestedSandbox: true` to fall back to reduced-capability mode that maintains isolation without nested namespaces. Not needed today but document it for when container-based workflows come up. + +Reference: [NVIDIA AI Workbench — Claude sandbox config](https://docs.nvidia.com/ai-workbench/user-guide/latest/quickstart/quickstart-claude-sandbox.html), [Docker Claude Code sandbox](https://docs.docker.com/ai/sandboxes/agents/claude-code/) + +--- + +## Longer-term + +### 5. Network allowlist + +Currently, the sandbox prompts for any new outbound domain. A future improvement: maintain an explicit allowlist of trusted domains in `settings.json` (`api.github.com`, `registry.npmjs.org`, etc.) and block unknown domains outright rather than prompting. Reduces interruptions without opening the network broadly. + +### 6. Per-project sandbox overrides + +Some projects need broader write access (e.g., a Docker-based project writing to `/var/lib/...` via `docker`). Mechanism: project-level `.claude/settings.json` with additive `allowWrite` entries that merge with the user-level config. + +--- + +## References + +- [Stop Using Default Settings — 10 Claude Code Configs That Actually Work](https://dev.to/shimo4228/stop-using-default-settings-10-claude-code-configs-that-actually-work-243l) +- [Fixing gh CLI in Claude Code's Sandbox](https://zencoder.ai/blog/fixing-gh-cli-in-claude-codes-sandbox) +- [Making Claude Code Actually Work Autonomously with Sandbox](https://www.linkedin.com/pulse/making-claude-code-actually-work-autonomously-sandbox-daniel-dimitrov-2khnf) +- [Railguard — per-command policy engine for Claude Code](https://github.com/railyard-dev/railguard) +- [Docker Claude Code Sandbox](https://docs.docker.com/ai/sandboxes/agents/claude-code/) +- [NVIDIA AI Workbench — Claude Code Quickstart](https://docs.nvidia.com/ai-workbench/user-guide/latest/quickstart/quickstart-claude-sandbox.html) diff --git a/agentic-ai/Claude/README.md b/agentic-ai/Claude/README.md new file mode 100644 index 0000000..aa540c5 --- /dev/null +++ b/agentic-ai/Claude/README.md @@ -0,0 +1,122 @@ +# Claude Code Config + +Version-controlled source of truth for `~/.claude/` settings, hooks, and rules. Running `install.sh` wires everything up via symlinks so changes here take effect immediately. + +## Activation + +**Wire up the config:** +```bash +bash agentic-ai/Claude/install.sh +``` + +This will: +- Back up your existing `~/.claude/settings.json` (if not already a symlink) +- Symlink `~/.claude/settings.json` → this `settings.json` +- Symlink `~/.claude/CLAUDE.md` → this `CLAUDE.md` +- Symlink `~/.claude/rules/` → this `rules/` +- Symlink each `hooks/*.sh` script into `~/.claude/hooks/` + +Restart Claude Code after running. + +> **Note:** `settings.json` sets `bypassPermissions` at the user level, so it applies to **all projects**, not just this repo. + +## What this configures + +### `bypassPermissions` +Claude auto-approves all tool calls without prompting. The hooks below act as the safety gate. + +### Sandbox +`sandbox.enabled` is currently **disabled**. Claude Code's Linux sandbox uses seccomp BPF to block all `AF_UNIX` socket calls — this breaks `gpg-agent` (required for commit signing) and `ssh-agent` (required for SSH push to GitHub, Bitbucket, Forgejo, etc.). Upstream issue [#44180](https://github.com/anthropics/claude-code/issues/44180) tracks the fix. The `denyRead`/`allowWrite` filesystem config is preserved in `settings.json` for re-enablement once the issue is resolved. + +The hooks below are the primary safety layer. + +### PreToolUse: `validate-bash.sh` (Bash) +Blocks dangerous or escalation-prone shell commands: +- `rm -rf` on `/` or `~` / `$HOME` +- `dd` targeting a device node +- `mkfs` (filesystem format) +- Redirect to block device (`> /dev/sdX`) +- Piped shell execution (`curl ... | sh`) +- Force-push to `main`/`master` +- `sudo` (escalation must be explicit — run yourself) +- `git add -A`, `git add --all`, `git add .` (bulk staging can silently include secrets) + +### PreToolUse: `validate-write.sh` (Write / Edit / MultiEdit) +Blocks writes to sensitive file paths: +- `~/.ssh/`, `~/.aws/`, `~/.gnupg/`, `~/.config/gh` +- `/etc/`, `/usr/`, `/boot/`, `/sys/`, `/proc/` + +### PostToolUse: `post-edit-shellcheck.sh` (Write / Edit / MultiEdit) +After any shell script edit, runs `shellcheck --severity=error`. Exits 2 if errors are found, forcing Claude to fix them before continuing. + +Skips gracefully if `shellcheck` is not installed. + +### Stop: `driftcheck.sh` +At session end, validates project conventions for all git-tracked `.sh` files: +- Execute permission set +- Shebang line present + +Exits 2 if violations found, injecting the list back into Claude's context. + +Hooks use **exit 2** to block — Claude receives the stderr message as the reason. + +## Rules (Tip 6 hierarchical structure) + +`CLAUDE.md` @-imports from `rules/` to keep principles modular: + +``` +rules/ + common/ + general.md — language-agnostic coding principles + agents.md — when to self-invoke Plan / Explore / review / verify + bash/ + style.md — bash scripting conventions +``` + +Add a new language by creating `rules//style.md` and adding an `@` line to `CLAUDE.md`. + +## Testing the hooks + +```bash +# Should exit 2 (blocked) +echo '{"tool_input":{"command":"rm -rf /"}}' | bash agentic-ai/Claude/hooks/validate-bash.sh +echo $? + +# Should exit 2 (blocked — sudo) +echo '{"tool_input":{"command":"sudo apt install foo"}}' | bash agentic-ai/Claude/hooks/validate-bash.sh +echo $? + +# Should exit 2 (blocked — bulk staging) +echo '{"tool_input":{"command":"git add -A"}}' | bash agentic-ai/Claude/hooks/validate-bash.sh +echo $? + +# Should exit 2 (blocked — redirect to sensitive path) +echo '{"tool_input":{"command":"echo foo > ~/.ssh/config"}}' | bash agentic-ai/Claude/hooks/validate-bash.sh +echo $? + +# Should exit 0 (allowed) +echo '{"tool_input":{"command":"ls -la"}}' | bash agentic-ai/Claude/hooks/validate-bash.sh +echo $? + +# Should exit 2 (blocked) +echo '{"tool_input":{"file_path":"/Users/ulises/.ssh/authorized_keys"}}' | bash agentic-ai/Claude/hooks/validate-write.sh +echo $? + +# Should exit 0 (allowed) +echo '{"tool_input":{"file_path":"/Users/ulises/github/project/main.py"}}' | bash agentic-ai/Claude/hooks/validate-write.sh +echo $? +``` + +## Security model + +With the sandbox disabled, Claude runs with your user's full filesystem access — the same security surface as Cursor, Copilot, or any terminal session. The hooks are the primary guardrail layer. + +**`validate-write.sh`** still blocks Claude's Write/Edit tools from touching `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gh`, and system paths. Bash commands can read those paths, which is intentional — GPG signing and `gh api` both require it. + +**`validate-bash.sh`** blocks catastrophic shell commands regardless of filesystem access. + +The operative trust model: `bypassPermissions` + hooks is a guardrail against accidental damage, not a zero-trust vault. Per-device SSH/GPG keys are the credential strategy — key material stays on the machine, not in a vault. + +## Adding settings + +All user-level Claude Code settings live here going forward. Edit `settings.json` directly — the symlink means changes are live immediately (no re-run of `install.sh` needed). diff --git a/agentic-ai/Claude/SANDBOX.md b/agentic-ai/Claude/SANDBOX.md new file mode 100644 index 0000000..e10f735 --- /dev/null +++ b/agentic-ai/Claude/SANDBOX.md @@ -0,0 +1,120 @@ +# Sandbox — State, Blockers, and Path Forward + +Claude Code's Linux sandbox (`bubblewrap` + seccomp BPF) is **currently disabled** (`sandbox.enabled: false`). This document records why, what was evaluated, and exactly what needs to happen before it can be re-enabled. + +--- + +## What the sandbox provides + +When enabled, the sandbox adds kernel-level isolation that hooks alone cannot replicate: + +- **`denyRead`** — prevents reading `~/.ssh`, `~/.aws`, and any other sensitive paths even if a hook is bypassed +- **`denyWrite`** — can block Bash-level writes to `/etc`, `/usr`, `/boot`, `/sys`, `/proc` (hooks only cover Write/Edit/MultiEdit tools, not raw shell redirects) +- **Network prompting** — prompts before the agent connects to any new outbound domain +- **Process isolation** — the bwrap mount namespace limits what child processes (npm, terraform, kubectl, etc.) can see and write + +Hooks catch dangerous commands; the sandbox catches everything that slips through at the OS level. Disabling it is a meaningful reduction in defense-in-depth, not just a papercut. + +--- + +## The blocker + +**Claude Code v2.1.92 bundled the `apply-seccomp` binary**, restoring a seccomp BPF filter that unconditionally blocks all `socket(AF_UNIX, ...)` syscalls inside sandboxed processes. + +This kills two hard requirements for personal dev workflows: + +| What breaks | Why AF_UNIX | Why it's required | +|---|---|---| +| `gpg-agent` | GPG 2.1+ always communicates with the agent via Unix socket; `--no-use-agent` is a deprecated no-op | Work requires signed commits | +| `ssh-agent` | `SSH_AUTH_SOCK` is a Unix socket; SSH falls back to key file, but `~/.ssh` is in `denyRead` | SSH is the only push method for GitHub, Bitbucket, Forgejo | + +There is no per-command seccomp exception. `excludedCommands` bypasses filesystem restrictions only ([#10524](https://github.com/anthropics/claude-code/issues/10524)). `settings.json` seccomp configuration is silently ignored ([#24238](https://github.com/anthropics/claude-code/issues/24238)). + +**Upstream tracking:** [#44180 — Linux (bwrap): Add allowUnixSockets / allowAllUnixSockets equivalent for seccomp BPF](https://github.com/anthropics/claude-code/issues/44180). Open, no timeline. + +--- + +## Everything evaluated as a workaround + +### `excludedCommands` +Bypasses sandbox **filesystem restrictions** only. The seccomp filter still applies. `git commit -S` and `git push` over SSH still fail. Not a solution. + +### `allowUnsandboxedCommands: true` +Equivalent to disabling the sandbox entirely — not a targeted workaround. Also does not bypass seccomp for specific commands. + +### `settings.json` seccomp config +Fields like `"seccomp": {}` are silently ignored ([#24238](https://github.com/anthropics/claude-code/issues/24238)). No-op. + +### SSH signing format (`gpg.format ssh`) instead of GPG +`git config gpg.format ssh` + `user.signingkey /path/to/key` makes git call `ssh-keygen -Y sign -f ` directly — a file read, no socket. This would bypass the seccomp block **for signing** but: +- GPG signatures are required (non-GitHub VCS: Bitbucket, Forgejo, and others do not support SSH-format signatures) +- The SSH login key lives in `~/.ssh` (in `denyRead`), and using a separate signing-only key at a different path would expose private key material to the agent + +Not viable given the full set of requirements. + +### SSH key file read (no agent, passphraseless key) +With passphraseless keys, SSH can read `~/.ssh/id_*` directly when `SSH_AUTH_SOCK` is unavailable. But `~/.ssh` is in `denyRead`. Moving the push key outside `~/.ssh` would expose private key material to the agent — acceptable blast radius for signing (agent already makes commits), but the GPG signing blocker remains regardless. + +### Docker sandbox +Claude Code runs inside a `docker/sandbox-templates:claude-code` container. Credentials live entirely outside the container via an OAuth proxy. **By design:** no SSH agent forwarding, no GPG forwarding, no access to `~/.config/gh`. Appropriate for CI/CD or untrusted-code execution — not for a personal dev machine that needs GPG signing over SSH remotes. Docs: [docs.docker.com/ai/sandboxes/agents/claude-code](https://docs.docker.com/ai/sandboxes/agents/claude-code/). + +### NVIDIA AI Workbench +Same bwrap + socat dual-layer stack. Adds `enableWeakerNestedSandbox: true` to handle nested user namespaces when Claude Code runs inside a Docker container (bwrap can't create namespaces inside containers without this). Also actively denies access to credential paths (`~/.claude.json`, `~/.claude/credentials.json`). Solves the "bwrap inside Docker" problem, not the AF_UNIX/seccomp problem. Docs: [docs.nvidia.com/ai-workbench/…/quickstart-claude-sandbox.html](https://docs.nvidia.com/ai-workbench/user-guide/latest/quickstart/quickstart-claude-sandbox.html). + +--- + +## Security model without sandbox + +Without bwrap, Claude runs as your user with full filesystem access — the same surface as any terminal session. Protection comes from: + +- **`validate-bash.sh`** — blocks `rm -rf /~`, `dd` to devices, `mkfs`, pipe-to-shell, force-push to main, `sudo`, bulk `git add` +- **`validate-write.sh`** — blocks Write/Edit/MultiEdit tools from touching `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gh`, `/etc`, `/usr`, `/boot`, `/sys`, `/proc` +- **OS file permissions** — Claude runs as your user; it can't access root-owned files regardless + +The gap relative to sandbox-enabled: a Bash command like `echo foo > /etc/hosts` bypasses `validate-write.sh` (hooks don't cover shell redirects) and there's no `denyRead` enforcement. These are theoretical risks rather than practical ones on a single-user dev machine, but they're real gaps. + +--- + +## Re-enable checklist + +When [#44180](https://github.com/anthropics/claude-code/issues/44180) ships: + +1. Check the release notes confirm `allowUnixSockets` (or equivalent) is available on Linux +2. In `settings.json`, flip `"enabled": false` → `"enabled": true` +3. Verify inside a Claude Code session: + ```bash + git commit --allow-empty -S -m "sandbox gpg test" # must sign without prompt + git push # must push via SSH + gh api user # must return your GitHub user + ``` +4. If GPG fails: check whether `/run/user/$UID/gnupg/S.gpg-agent` needs to be added to `allowUnixSockets` +5. If SSH fails: check whether `$SSH_AUTH_SOCK` path needs to be in `allowUnixSockets` +6. Once verified: also add `~/.config/gh` back to `denyRead` and configure `GH_TOKEN` in `settings.json` env (see [PLAN.md](PLAN.md) item 2) + +The `denyRead`/`allowWrite` filesystem config is already in place in `settings.json` and requires no changes. + +--- + +## `enableWeakerNestedSandbox` — future note + +If Claude Code ever runs inside a Docker container on this machine, add this to `settings.json`: + +```json +"sandbox": { + "enabled": true, + "enableWeakerNestedSandbox": true +} +``` + +bwrap cannot create user namespaces inside an unprivileged container; this flag falls back to a reduced-capability mode that maintains isolation without nested namespaces. Not needed for bare-metal / VM setups. + +--- + +## Upstream issues to watch + +| Issue | Description | +|---|---| +| [#44180](https://github.com/anthropics/claude-code/issues/44180) | **Primary blocker** — Linux: add `allowUnixSockets` equivalent for seccomp BPF | +| [#41817](https://github.com/anthropics/claude-code/issues/41817) | Path-scoped Unix socket support (macOS) — Linux parity likely follows | +| [#10524](https://github.com/anthropics/claude-code/issues/10524) | `excludedCommands` bypasses filesystem only, not seccomp | +| [#24238](https://github.com/anthropics/claude-code/issues/24238) | seccomp config in settings.json silently ignored | diff --git a/agentic-ai/Claude/hooks/driftcheck.sh b/agentic-ai/Claude/hooks/driftcheck.sh new file mode 100755 index 0000000..d0eadd4 --- /dev/null +++ b/agentic-ai/Claude/hooks/driftcheck.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Stop hook: validates project conventions before Claude finishes a session. +# Exit 2 = block stop (stderr injected back into Claude; must fix and try again). +# +# Checks git-tracked .sh files for consistency: +# - has shebang but not executable → flag (meant to run but can't) +# - is executable but no shebang → flag (can run but no interpreter declared) +# Library/sourced files (no shebang, not executable) are intentionally skipped. +set -euo pipefail +trap 'exit 2' ERR + +git rev-parse --git-dir &>/dev/null || exit 0 + +issues=() + +while IFS= read -r f; do + [[ -f "$f" ]] || continue + read -r first_line < "$f" || first_line="" + has_shebang=false; is_exec=false + [[ "$first_line" == '#!'* ]] && has_shebang=true + [[ -x "$f" ]] && is_exec=true + + if $has_shebang && ! $is_exec; then + issues+=("has shebang but missing execute permission: $f") + elif $is_exec && ! $has_shebang; then + issues+=("is executable but missing shebang: $f") + fi +done < <(git ls-files '*.sh') + +if [[ ${#issues[@]} -gt 0 ]]; then + printf 'driftcheck.sh: convention violations found:\n' >&2 + printf ' - %s\n' "${issues[@]}" >&2 + printf 'Fix these before finishing.\n' >&2 + exit 2 +fi diff --git a/agentic-ai/Claude/hooks/post-edit-shellcheck.sh b/agentic-ai/Claude/hooks/post-edit-shellcheck.sh new file mode 100755 index 0000000..2592af6 --- /dev/null +++ b/agentic-ai/Claude/hooks/post-edit-shellcheck.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# PostToolUse hook for Write/Edit/MultiEdit: runs shellcheck on edited .sh files. +# Exit 2 = block (Claude sees stderr and must fix before continuing). +set -euo pipefail +trap 'exit 2' ERR + +command -v shellcheck &>/dev/null || exit 0 + +INPUT=$(cat) +FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""') + +[[ "$FILE" == *.sh ]] || exit 0 +[[ -f "$FILE" ]] || exit 0 + +if ! shellcheck --severity=error "$FILE" >&2; then + printf '\npost-edit-shellcheck.sh: shellcheck errors in %s — fix before continuing.\n' "$FILE" >&2 + exit 2 +fi diff --git a/agentic-ai/Claude/hooks/post-test-runner.sh b/agentic-ai/Claude/hooks/post-test-runner.sh new file mode 100755 index 0000000..400a4c0 --- /dev/null +++ b/agentic-ai/Claude/hooks/post-test-runner.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# PostToolUse hook for Write/Edit/MultiEdit: runs the project test suite after source edits. +# Exit 2 = Claude sees failure output (warn). Exit 0 = passed (timing shown) or no suite found. +set -uo pipefail + +INPUT=$(cat) || exit 0 +FILE=$(jq -r '.tool_input.file_path // ""' <<< "$INPUT" 2>/dev/null) || exit 0 +[[ -n "$FILE" ]] || exit 0 + +# Skip non-source extensions +case "${FILE##*.}" in + md|txt|json|yaml|yml|toml|lock|rst|svg|png|jpg|jpeg|gif|pdf|ico) exit 0 ;; +esac + +# Resolve project root via git; no repo = no test suite +ROOT=$(git -C "$(dirname "$FILE")" rev-parse --show-toplevel 2>/dev/null) || exit 0 + +# Discover test command: .claude/test-cmd override → auto-detect → skip +TEST_CMD="" +if [[ -f "$ROOT/.claude/test-cmd" ]]; then + TEST_CMD=$(< "$ROOT/.claude/test-cmd") +elif [[ -f "$ROOT/Cargo.toml" ]]; then + TEST_CMD="cargo test" +elif [[ -f "$ROOT/go.mod" ]]; then + TEST_CMD="go test ./..." +elif [[ -f "$ROOT/pyproject.toml" || -f "$ROOT/pytest.ini" || -f "$ROOT/setup.py" ]]; then + TEST_CMD="pytest" +elif [[ -f "$ROOT/package.json" ]]; then + _npm_test=$(jq -r '.scripts.test // ""' "$ROOT/package.json" 2>/dev/null) || true + [[ -n "$_npm_test" && "$_npm_test" != *'no test specified'* ]] && TEST_CMD="npm test" +elif [[ -f "$ROOT/Makefile" ]] && grep -q '^test[[:space:]]*:' "$ROOT/Makefile" 2>/dev/null; then + TEST_CMD="make test" +fi + +[[ -n "$TEST_CMD" ]] || exit 0 + +TMPFILE=$(mktemp) +trap 'rm -f "$TMPFILE"' EXIT + +START_MS=$(date +%s%3N) +(cd "$ROOT" && timeout 60 bash -c "$TEST_CMD") > "$TMPFILE" 2>&1 +EXIT_CODE=$? +END_MS=$(date +%s%3N) +ELAPSED_MS=$(( END_MS - START_MS )) +ELAPSED_FMT=$(printf '%d.%03ds' $(( ELAPSED_MS / 1000 )) $(( ELAPSED_MS % 1000 ))) + +if [[ $EXIT_CODE -eq 0 ]]; then + printf 'post-test-runner: %s passed (%s)\n' "$TEST_CMD" "$ELAPSED_FMT" >&2 +elif [[ $EXIT_CODE -eq 124 ]]; then + printf 'post-test-runner: %s timed out after 60s — consider a longer timeout in .claude/test-cmd\n' "$TEST_CMD" >&2 + exit 2 +else + printf 'post-test-runner: %s failed (%s)\n' "$TEST_CMD" "$ELAPSED_FMT" >&2 + cat "$TMPFILE" >&2 + exit 2 +fi diff --git a/agentic-ai/Claude/hooks/validate-bash.sh b/agentic-ai/Claude/hooks/validate-bash.sh new file mode 100755 index 0000000..e406c9b --- /dev/null +++ b/agentic-ai/Claude/hooks/validate-bash.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# PreToolUse hook for Bash: blocks dangerous command patterns. +# Exit 2 = block the tool call (stderr is shown to Claude as the reason). +set -euo pipefail +trap 'exit 2' ERR + +COMMAND=$(jq -r '.tool_input.command // ""') +# Heredoc bodies are message text, not executed code — policy checks use only the first line. +FIRST_LINE=$(head -1 <<< "$COMMAND") + +block() { + printf 'validate-bash.sh blocked: %s\n' "$1" >&2 + exit 2 +} + +# rm -rf targeting root or home directory +if grep -qE 'rm[[:space:]]+-[a-zA-Z]*(rf|fr)[a-zA-Z]*' <<< "$COMMAND" \ + && grep -qE '(^|[[:space:]])(\/|~\/?|\$HOME\/?)([[:space:]]|$)' <<< "$COMMAND"; then + block "rm -rf on root or home directory" +fi + +# dd writing to a raw device node +grep -qE '\bdd\b.*\bof=/dev/' <<< "$COMMAND" && block "dd targeting a device node" + +# filesystem format +grep -qE '(^|[[:space:]])mkfs([[:space:]]|$)' <<< "$COMMAND" && block "mkfs would format a filesystem" + +# redirect to block device +grep -qE '>[[:space:]]*/dev/sd' <<< "$COMMAND" && block "redirect to block device" + +# redirect to sensitive credential / system paths (covers the gap that validate-write.sh can't close for shell redirects) +grep -qE '>{1,2}[[:space:]]*((~|\$HOME)/\.(ssh|aws|gnupg|config/gh)|/(etc|usr|boot|sys|proc))' <<< "$COMMAND" \ + && block "redirect to sensitive path" + +# piped shell execution (curl/wget | sh) +grep -qE '(curl|wget)[[:space:]].*\|[[:space:]]*(sudo[[:space:]]+)?(ba)?sh\b' <<< "$COMMAND" \ + && block "piped shell execution (curl/wget | sh)" +grep -qE '(curl|wget)[[:space:]].*\|[[:space:]]*(python3?|ruby|node|perl)\b' <<< "$COMMAND" \ + && block "piped interpreter execution (curl/wget | interpreter)" + +# force-push to main/master +grep -qE 'git[[:space:]]+push[[:space:]].*(-f\b|--force\b).*(main|master)' <<< "$COMMAND" \ + && block "force-push to main/master" +grep -qE 'git[[:space:]]+push[[:space:]].*(main|master).*(-f\b|--force\b)' <<< "$COMMAND" \ + && block "force-push to main/master" + +# sudo escalation (must be explicit, not autonomous) +grep -qE '(^|[^[:alnum:]_])sudo[[:space:]]' <<< "$FIRST_LINE" \ + && block "sudo: escalation must be explicit — run the command yourself" + +# git add -A / --all / . (bulk staging can silently include secrets) +grep -qE 'git[[:space:]]+add[[:space:]]+(-A\b|--all\b)' <<< "$FIRST_LINE" \ + && block "git add -A/--all — stage specific files instead" +grep -qE 'git[[:space:]]+add[[:space:]]+\.([[:space:]]|$)' <<< "$FIRST_LINE" \ + && block "git add . — stage specific files instead" + +# python -c with subprocess/os.system (scoped to FIRST_LINE — same heredoc reason as sudo/git-add) +grep -qE 'python3?[[:space:]]+-c' <<< "$FIRST_LINE" \ + && grep -qE 'os\.(system|exec[vl]|popen)|subprocess\.(Popen|check_output|call|run)' <<< "$FIRST_LINE" \ + && block "python -c: inline subprocess/shell execution" + +# node -e with child_process +grep -qE 'node[[:space:]]+-e' <<< "$FIRST_LINE" \ + && grep -qE "require\(['\"]child_process|execSync[[:space:]]*\(|spawnSync[[:space:]]*\(" <<< "$FIRST_LINE" \ + && block "node -e: child_process execution" + +# perl -e with shell execution +grep -qE 'perl[[:space:]]+-e' <<< "$FIRST_LINE" \ + && grep -qE '(^|[^[:alnum:]_])(system|exec)[[:space:]]*\(' <<< "$FIRST_LINE" \ + && block "perl -e: inline shell execution" + +exit 0 diff --git a/agentic-ai/Claude/hooks/validate-write.sh b/agentic-ai/Claude/hooks/validate-write.sh new file mode 100755 index 0000000..96d3c63 --- /dev/null +++ b/agentic-ai/Claude/hooks/validate-write.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# PreToolUse hook for Write/Edit/MultiEdit: blocks writes to sensitive file paths. +# Exit 2 = block the tool call (stderr is shown to Claude as the reason). +set -euo pipefail +trap 'exit 2' ERR + +INPUT=$(cat) +FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""') + +block() { + printf 'validate-write.sh blocked: %s\n' "$1" >&2 + exit 2 +} + +# Expand leading ~ to $HOME for comparison +EXPANDED="${FILE_PATH/#\~/$HOME}" + +SENSITIVE_PREFIXES=( + "$HOME/.ssh" + "$HOME/.aws" + "$HOME/.gnupg" + "$HOME/.config/gh" + "/etc" + "/usr" + "/boot" + "/sys" + "/proc" +) + +for prefix in "${SENSITIVE_PREFIXES[@]}"; do + if [[ "$EXPANDED" == "$prefix"* ]]; then + block "write to sensitive path: $FILE_PATH" + fi +done + +exit 0 diff --git a/agentic-ai/Claude/install.sh b/agentic-ai/Claude/install.sh new file mode 100755 index 0000000..4726fad --- /dev/null +++ b/agentic-ai/Claude/install.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Idempotent setup: symlinks this repo's Claude config into ~/.claude/ +# Safe to re-run. Backs up any existing settings.json before replacing it. + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLAUDE_DIR="$HOME/.claude" +HOOKS_DIR="$CLAUDE_DIR/hooks" +SETTINGS="$CLAUDE_DIR/settings.json" + +printf 'Installing from: %s\n' "$REPO_DIR" + +# Back up settings.json if it exists and is not already one of ours +if [[ -e "$SETTINGS" && ! -L "$SETTINGS" ]]; then + BACKUP="$SETTINGS.bak.$(date +%Y%m%d%H%M%S)" + printf 'Backing up existing settings.json → %s\n' "$BACKUP" + mv "$SETTINGS" "$BACKUP" +fi + +ln -sf "$REPO_DIR/settings.json" "$SETTINGS" +printf 'Linked: settings.json\n' + +# Symlink CLAUDE.md +ln -sf "$REPO_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md" +printf 'Linked: CLAUDE.md\n' + +# Symlink rules directory (used by @imports in CLAUDE.md) +# rm -f first: ln -sf on an existing dir-symlink creates a nested link inside it +rm -f "$CLAUDE_DIR/rules" +ln -sf "$REPO_DIR/rules" "$CLAUDE_DIR/rules" +printf 'Linked: rules/\n' + +# Symlink railguard policy (global: find_policy_file walks up from cwd) +ln -sf "$REPO_DIR/railguard.yaml" "$HOME/.railguard.yaml" +printf 'Linked: railguard.yaml → ~/.railguard.yaml\n' + +# Create hooks dir if it doesn't exist +mkdir -p "$HOOKS_DIR" + +# Symlink each hook script and ensure it's executable +for hook in "$REPO_DIR/hooks/"*.sh; do + chmod +x "$hook" + ln -sf "$hook" "$HOOKS_DIR/$(basename "$hook")" + printf 'Linked: hooks/%s\n' "$(basename "$hook")" +done + +# Warn on Ubuntu 24.04+ if the bwrap AppArmor profile isn't set up +if grep -qi 'ubuntu' /etc/os-release 2>/dev/null && ! [[ -f /etc/apparmor.d/bwrap ]]; then + printf '\n' + printf '⚠ Ubuntu detected: run setup-linux-sandbox.sh (with sudo) to enable sandboxing.\n' + printf ' bash %s/setup-linux-sandbox.sh\n' "$REPO_DIR" +fi + +printf '\nDone. Restart Claude Code for changes to take effect.\n' diff --git a/agentic-ai/Claude/railguard.yaml b/agentic-ai/Claude/railguard.yaml new file mode 100644 index 0000000..0326e69 --- /dev/null +++ b/agentic-ai/Claude/railguard.yaml @@ -0,0 +1,41 @@ +# Railguard Policy — Computer-Setup / global +# https://github.com/railyard-dev/railguard +# +# Defaults are prepended at runtime (merge_with_defaults); only overrides here. +# Rules evaluated: allowlist → blocklist → approve → default allow. + +version: 1 + +# ── Custom blocklist additions ──────────────────────────────────────── +# escalation and bulk-staging blocks were removed — validate-bash.sh owns +# those patterns with first-line scoping to avoid heredoc false positives. + +blocklist: [] + +approve: [] +allowlist: [] + +# ── Path fence ──────────────────────────────────────────────────────── +# Project directory (CWD) is always implicitly allowed. +# ~/.gnupg and ~/.config/gh are intentionally absent — required for GPG +# commit signing and gh CLI; managed at the validate-write.sh hook layer. + +fence: + enabled: true + allowed_paths: + - "~/.claude" + - "/tmp" + denied_paths: + - "~/.ssh" + - "~/.aws" + - "~/.config/gcloud" + - "/etc" + +trace: + enabled: true + directory: .railguard/traces + +snapshot: + enabled: true + tools: [Write, Edit] + directory: .railguard/snapshots diff --git a/agentic-ai/Claude/rules/bash/style.md b/agentic-ai/Claude/rules/bash/style.md new file mode 100644 index 0000000..5580b9d --- /dev/null +++ b/agentic-ai/Claude/rules/bash/style.md @@ -0,0 +1,11 @@ +# Bash Style Rules + +- Shebang: always `#!/usr/bin/env bash` (not `/bin/bash`). +- Set at the top of every non-trivial script: `set -euo pipefail`. +- Use `[[ ]]` not `[ ]` for conditionals. +- Quote all variable expansions: `"$var"`, `"${arr[@]}"`. +- Use `printf` not `echo` for reliable, portable output. +- Declare function-local variables with `local`. +- Write error messages to stderr: `printf 'error: %s\n' "$msg" >&2`. +- Prefer `command -v foo` over `which foo` to check for executables. +- Use `<<< "$var"` (herestring) instead of `echo "$var" |` to avoid a subshell. diff --git a/agentic-ai/Claude/rules/common/agents.md b/agentic-ai/Claude/rules/common/agents.md new file mode 100644 index 0000000..500278e --- /dev/null +++ b/agentic-ai/Claude/rules/common/agents.md @@ -0,0 +1,23 @@ +# Agent Self-Activation Rules + +Invoke specialized agents proactively — don't wait to be asked. + +## When to use the Plan agent + +Before starting any non-trivial implementation that spans multiple files or requires architectural decisions, spawn an `Agent` with `subagent_type: "Plan"` to design the approach first. + +## When to use the Explore agent + +For codebase searches that require more than 3 targeted lookups, or open-ended "where is X / what references Y" questions, spawn `subagent_type: "Explore"` to protect the main context from excessive search results. + +## When to run /review + +After making substantive code changes on a branch, use the `review` skill to catch issues before the PR is opened. + +## When to run /security-review + +Before committing changes that touch authentication, secrets handling, file system paths, network requests, or shell execution — run the `security-review` skill. + +## When to run /verify + +After implementing a fix or feature, use the `verify` skill to confirm the change works in the running app, not just in tests. diff --git a/agentic-ai/Claude/rules/common/general.md b/agentic-ai/Claude/rules/common/general.md new file mode 100644 index 0000000..4d67440 --- /dev/null +++ b/agentic-ai/Claude/rules/common/general.md @@ -0,0 +1,9 @@ +# General Coding Principles + +- Write no comments by default. Add one only when the WHY is non-obvious: a hidden constraint, a workaround for a specific bug, a subtle invariant. +- Don't explain WHAT the code does — well-named identifiers already do that. +- Don't design for hypothetical future requirements. Three similar lines beats a premature abstraction. +- No error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees; validate only at system boundaries (user input, external APIs). +- Prefer editing existing files to creating new ones. +- Default to not adding features, refactors, or abstractions beyond what the task requires. +- No backwards-compatibility shims for clearly removed or unused code — delete it cleanly. diff --git a/agentic-ai/Claude/settings.json b/agentic-ai/Claude/settings.json new file mode 100644 index 0000000..0fe588c --- /dev/null +++ b/agentic-ai/Claude/settings.json @@ -0,0 +1,109 @@ +{ + "permissions": { + "defaultMode": "bypassPermissions" + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash ~/.claude/hooks/validate-bash.sh" + } + ] + }, + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "bash ~/.claude/hooks/validate-write.sh" + } + ] + }, + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "~/.cargo/bin/railguard hook --event PreToolUse", + "timeout": 5000 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "bash ~/.claude/hooks/post-edit-shellcheck.sh" + }, + { + "type": "command", + "command": "bash ~/.claude/hooks/post-test-runner.sh", + "timeout": 75000 + } + ] + }, + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "~/.cargo/bin/railguard hook --event PostToolUse", + "timeout": 5000 + } + ] + } + ], + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "~/.cargo/bin/railguard hook --event SessionStart", + "timeout": 5000 + } + ] + } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash ~/.claude/hooks/driftcheck.sh" + } + ] + } + ] + }, + "sandbox": { + "enabled": false, + "allowUnsandboxedCommands": false, + "filesystem": { + "allowWrite": [ + "~/.gnupg", + "~/.config/gh" + ], + "denyWrite": [ + "/etc", + "/usr", + "/boot", + "/sys", + "/proc" + ], + "denyRead": [ + "~/.ssh", + "~/.aws" + ] + } + }, + "advisorModel": "opus", + "skipDangerousModePermissionPrompt": true +} diff --git a/agentic-ai/Claude/setup-linux-sandbox.sh b/agentic-ai/Claude/setup-linux-sandbox.sh new file mode 100755 index 0000000..512ad75 --- /dev/null +++ b/agentic-ai/Claude/setup-linux-sandbox.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# One-time OS-level setup for Claude Code sandboxing on Ubuntu 24.04+. +# Ubuntu 24.04's AppArmor policy blocks bwrap from creating user namespaces; +# this profile grants exactly that capability and nothing else. +# Requires sudo. Safe to re-run. + +set -euo pipefail + +if ! command -v bwrap &>/dev/null; then + printf 'bubblewrap not found. Install it first:\n' + printf ' Ubuntu/Debian: sudo apt-get install bubblewrap socat\n' + printf ' Fedora: sudo dnf install bubblewrap socat\n' + exit 1 +fi + +PROFILE="/etc/apparmor.d/bwrap" + +if [[ -f "$PROFILE" ]]; then + printf 'AppArmor profile already exists: %s\n' "$PROFILE" + exit 0 +fi + +if ! command -v apparmor_status &>/dev/null; then + printf 'AppArmor not detected — profile not required on this system.\n' + exit 0 +fi + +printf 'Writing AppArmor profile: %s\n' "$PROFILE" +sudo tee "$PROFILE" > /dev/null <<'EOF' +abi , +include + +profile bwrap /usr/bin/bwrap flags=(unconfined) { + userns, + include if exists +} +EOF + +printf 'Reloading AppArmor...\n' +sudo systemctl reload apparmor +printf 'Done. Restart Claude Code to activate sandboxing.\n'