Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
]
},
{
"matcher": "Read",
"matcher": "Read|Grep|Glob|NotebookRead",
"hooks": [
{
"type": "command",
Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,54 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- **`protect-paths` matches secret paths per path token, so read-only commands stop being
blocked.** `\.env(\.[\w-]+)?\b` matched `.env` anywhere in a Bash command, so
`grep -rn process.env src`, `rg 'import\.meta\.env'`, `git log --grep='.env handling'`,
`cat messages.key.ts` and Reads of `.env.example`, a plain project `.npmrc` and a Next.js
`app/docs/secrets/page.tsx` route were all refused. The command is now split into shell
words (quotes honoured) and only each command's file operands are tested; a grep/rg/sed/awk/jq
pattern, a commit message or a `--grep=` value is never a path. `.env.example`/`.sample`/
`.template`/`.dist` are templates, `*.key.ts` is code, a `secrets/` directory's source files
are code, and a project `.npmrc` is protected only when it holds a literal `_authToken`/
`_auth`/`_password` (an `${NPM_TOKEN}` reference is not a secret); `~/.npmrc` always is, and
so is a relative `.npmrc` once the command has changed directory (`cd ~ && cat .npmrc`).
`.env`, `.env.local`, `.env-local`, `.env.production`, `id_rsa`, `*.key` and `*.pem` stay
blocked. For a `settings.json` install, `permissions.deny` still lists `Read(./.env.*)` and
`Read(./**/.npmrc)`, and deny wins, so the Read tool keeps refusing `.env.example` and a
token-less `.npmrc` there; the relaxation reaches Bash everywhere and the Read tool on a
plugin install.
- **`protect-paths` closes the reads it missed.** `sed`, `awk`, `tac`, `sort`, `uniq`, `cut`,
`paste`, `bat`, `jq`, `diff`, `cmp`, `fold`, `rev`, `hexdump` and `dd if=` now count as
readers; `… < .env` (input redirection), `cat .e*v` (globs), `cat .{env,x}` and
`--include '*.{env,pem}'` (brace alternation), `$( … )`/backticks, `$(echo .env)`,
`$'\x2eenv'`, `bash -c '…'`/`eval …` strings, a command on a second line (also after
`$((1<<2))`, which is a shift and not a heredoc) and a heredoc fed to a shell, wrapped or not
(`sudo bash <<EOF`), are checked. A flag between a reader or writer and its file no longer
hides the file (`cat -n .env`, `sort -n .env`, `cp -n .env x`), a pattern glued to its option
is read as the pattern (`grep -eKEY .env`), index paths are checked (`git show :.env`), and a
recursive read of a `secrets/` directory is blocked (`rg KEY secrets/`, `git diff -- secrets/`,
the Grep tool unless its `glob`/`type` keeps it to source files). `rm -rf` of `.`, `..`, `./` or `*`,
`git checkout .` / `git restore .` (but not `git restore --staged .`), and
`curl … | sudo sh` / `| python3` are blocked. The protect-paths matcher now covers `Read`,
`Grep`, `Glob` and `NotebookRead` in all three hook manifests; the Grep tool's `glob` filter
is checked too.
- **`protect-paths` fails closed and no longer needs bash.** With no bash on `PATH` the hook
launcher exited 1, which Claude Code treats as a non-blocking error, so the guard was silently
off (a signal-killed guard did the same). `run.mjs` now runs `protect-paths.mjs` on its own
node, and any failure to reach a verdict (no interpreter, a spawn error, a signal, an exit
other than 0/2) blocks with exit 2. `node run.mjs --fail-closed <guard>.sh` opts any other
guard in.
- **`forge doctor` no longer asks a plugin user to register every guard twice.** It ignored
`enabledPlugins` and reported "forge hooks missing/stale (15/15 guard(s) absent) — run forge
doctor --fix", and that fix merged the same hooks into `settings.json` on top of the plugin's
`hooks/hooks.json`, so every guard ran twice. With `forgekit@…` enabled (user, project or
local settings), doctor reports "guards via the forgekit plugin", its fix merges permissions
only, and a settings copy of the guards is reported as a double registration. `forge init`
reads the same user, project and local scopes and skips hook injection when the plugin is
enabled in any of them (`mergeSettings` alone skips it for a settings file that enables it).

## [1.4.0] - 2026-09-24

### Fixed
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ delivers them into every tool you use.
> assume **Bash and Git** are available (`jq` is not required — the guards read hook JSON
> with node). Claude hooks on Windows do
> not require `bash` on `PATH`: their Node launcher finds Git Bash and preserves guard exits.
> `protect-paths` needs no bash at all and fails closed: when it cannot reach a verdict, the
> tool call is blocked rather than let through.

## Start in 60 seconds
Forgekit is a beta Node.js CLI and MCP server for AI-assisted software development. It
Expand Down Expand Up @@ -386,7 +388,10 @@ forge doctor --fix
`~/.claude/settings.json`. That file is global and affects all repositories. Use
`forge init --no-settings` to skip the merge or `forge init --remove-settings` to reverse
Forgekit-managed entries. The implementation preserves unrelated entries and creates a
timestamped backup before changing the file.
timestamped backup before changing the file. When the Forgekit Claude Code plugin is enabled
(in user, project or local settings), its `hooks/hooks.json` already runs every guard, so
`forge init` and `forge doctor --fix` merge permissions only and never register the guards a
second time.

`forge init` emits configuration only for the agent tools the repository already uses:
Claude Code plus any tool with a sign on disk (`.cursor/`, `.codex/`,
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ forgekit's controls map to the 2026 baselines:
- Prompt injection / supply chain (LLM01/03/05): `forge scan` (skill-gate) blocks injection /
RCE / exfil in a skill or MCP config **before** install.
- Insecure output handling / sensitive-info disclosure (LLM02/06): the `secret-redact` guard
masks keys in tool output; `protect-paths` blocks secret-file reads/writes.
masks keys in tool output; `protect-paths` blocks secret-file reads/writes (Read, Grep,
Glob, NotebookRead, edits and Bash) and fails closed when it cannot reach a verdict.
- Excessive agency (LLM08): guards enforce least privilege + human-in-the-loop
(`permissionDecision` deny/ask); `forge harden` wires the OS sandbox.
- Unbounded consumption / model DoS (LLM10): the cost governor + doom-loop breaker cap runaway
Expand Down
29 changes: 28 additions & 1 deletion docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,7 @@ Plain `forge cost` remains the per-day spend view via `ccusage`.
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `forge init` | Emit native config for the tools this repo uses — Claude + detected ones, or `--tools <list|all>` (recorded for later syncs). In `AGENTS.md` forge owns only its `<!-- forge:begin/end -->` block; a hand-written file keeps every line. |
| `forge sync` | Recompile `source/` → each selected tool's files (idempotent); rewrites only forge's block in `AGENTS.md`. Size checks measure the whole `AGENTS.md`; warns while an older `AGENTS.md.forge-bak` holds rules no agent reads. |
| `forge doctor` | Health check: layers, install, drift, cortex. `forge doctor --fix` auto-repairs the safely fixable findings (missing settings hooks/permissions, ledger union-merge rule, stale `AGENTS.md`, non-executable guards) by reusing the same idempotent functions `forge init`/`forge sync` use, then re-checks. |
| `forge doctor` | Health check: layers, install, drift, cortex. `forge doctor --fix` auto-repairs the safely fixable findings (missing settings hooks/permissions — permissions only when the Forge plugin is enabled, since the plugin already wires the hooks — ledger union-merge rule, stale `AGENTS.md`, non-executable guards) by reusing the same idempotent functions `forge init`/`forge sync` use, then re-checks. |
| `forge docs check` | Docs↔code drift: commands, env vars, MCP tools, CHANGELOG, and the Mintlify site (`mintlify/`) reconciled against the code (CI-gated on the forge repo itself). |
| `forge docs sync` | Diff-driven stale-docs sweep: UPDATED / STALE (file:line hits) / VERIFIED-UNAFFECTED per artifact (see the full section above). |
| `forge docs impact` | Documentation-reference graph: extract typed entities (commands/flags/env/MCP/symbols/version), index every doc surface, and report which docs reference the entities THIS diff changed — ranked by confidence (see the full section above). |
Expand Down Expand Up @@ -1397,6 +1397,33 @@ untouched, so a guard's exit 2 still blocks. `forge doctor` shows which bash it
flags hooks left in the old `bash …` spelling; `forge doctor --fix` (or `forge init`) heals
Forge-owned ones in place. On macOS/Linux nothing changes: `bash` from `PATH`, as before.

**Protected paths (PreToolUse).** `protect-paths` runs on `Edit`/`Write`/`MultiEdit`/
`NotebookEdit`/`Bash` and on the readers `Read`/`Grep`/`Glob`/`NotebookRead`. It blocks secret
files (`.env`, `.env.local`, `.env-local`, `.env.production`, `prod.env`, `id_rsa`, `*.key`,
`*.pem`, anything under `.ssh/` or a `secrets/` store, `.netrc`, `.git-credentials`,
`.aws/credentials`, the user-level `~/.npmrc`, and a project `.npmrc` only when it holds a
literal token) plus clearly destructive shell (`rm -rf` of `/`, `~`, `.`, `..` or `*`;
`git reset --hard`; `git checkout .` and `git restore .`; force-push; pipe-to-shell). A Bash
command is split into shell words and only each command's FILE operands are tested, so
`grep -rn process.env src`, `rg 'import\.meta\.env'`, `cat .env.example` and
`cat messages.key.ts` pass, while `sed -n p .env`, `cat -n .env`, `grep -eKEY .env`,
`git show :.env`, `… < .env`, `cat .e*v`, `cat .{env,x}` and `bash -c 'cat .env'` are blocked.
A flag between a reader and its file never hides the file, and after a `cd` a relative
`.npmrc` is treated as the user-level one. A recursive read of a `secrets/` directory
(`rg KEY secrets/`, `git diff -- secrets/`, the Grep tool) is blocked unless the Grep tool's `glob`/`type` keeps it to
source files (`*.tsx`, `ts`), so a Next.js `app/…/secrets/page.tsx` route stays searchable. The
launcher runs it on Node directly (no bash needed) and fails **closed**: a guard that cannot
reach a verdict — no interpreter, a crash, a signal — blocks the call (exit 2) instead of
exiting 1, which Claude Code treats as a non-blocking error. `node run.mjs --fail-closed
<guard>.sh` opts any other guard into the same behaviour. What passes the guard can still be
denied by Claude Code itself: a `settings.json` install keeps `Read(./.env.*)` and
`Read(./**/.npmrc)` in `permissions.deny`, and deny always wins, so there a Read of
`.env.example` or of a token-less `.npmrc` is still refused (Bash `cat` of them is not; a plugin
install has no such deny list). It is pattern matching, not a sandbox: variable indirection
(`f=.env; cat $f`), `find … -exec cat`, `xargs -a`, command substitutions other than a literal
`$(echo …)`, and interpreters (`python -c`, `node -e`) are out of scope, and secret-redact still
masks leaked values after the tool runs.

Three more ambient layers ride the same hooks:

**Session rehydration (SessionStart).** Besides lessons and the anchored goal, every
Expand Down
Loading
Loading