Second review cycle, security + checklist:
- IMPECCABLE_BIN=/bin/sh (or node) was READY, and `detect` with cwd=repoRoot
made the interpreter run the repository's own `detect` file. Every engine
candidate (env override, PATH entry, cache, sibling) is now judged by the
realpath of the FILE and must be named impeccable[.exe]; PATH and cache
candidates that resolve into the repository are skipped like the others.
"Inside the project" means the repository, or cwd when cwd is a project
directory: HOME and its ancestors are exempt, so a URL-mode review launched
from HOME still finds the HOME-rooted installs.
- A base for --changed that starts with `-` was spliced into git argv
(`--output=<file>` made git write a file and report no changes); an option-
like or missing base is DETECT_REFUSED (not a ref name), exit 1, and the
parser no longer defaults a missing value to main.
- DOM dumps are the audited page's bytes, so an in-file `impeccable-disable`
comment there is page-controlled: batches under the designs root run with
--no-inline-ignores, repository batches keep the project's own ignores.
- neutralizeSentinels covers the shapes it missed (bare sentinels such as
DETECT_TOP total= and IMPECCABLE_DISABLED, the DETECT_EXIT_CODE= echo, the
`[rule-id] impact=` group header) in one precompiled alternation instead of
37 replaceAll passes per field; only kept findings are normalized, and the
summary's total stays the engine's count.
- The minimal engine environment compares keys case-insensitively on Windows
(process.env enumerates Path, SystemRoot there) and passes PATHEXT, COMSPEC,
HOMEDRIVE, HOMEPATH, PROGRAMDATA.
- Bare 64s move into DETECT_LIMITS; the unused SentinelName type is gone; the
header states the directory-target contract (the engine's own walk).
Tests: an interpreter as IMPECCABLE_BIN never runs the repo's detect file; a
PATH symlink into the repository is never READY; option-like and empty bases
are refused with no file written; the designs-root batch carries
--no-inline-ignores and the repo batch does not; the identity label is
deterministic per binary; the bare-sentinel and header shapes are neutralized;
the installed fake engine works without IMPECCABLE_FAKE_OUTPUT (the helper
copies the sample beside it); two tests clean up in finally.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pre-landing review findings (security + checklist), all reproduced before the fix:
- A checked-out branch could commit `.claude/skills/impeccable/scripts/bin/<os>-<arch>/impeccable`
and the probe would report READY and `scan` would run it, with the agent's full
environment. Launchers and sibling engines under the repo or cwd now count as
"skill present" only (IMPECCABLE_NOT_CACHED: repository-local install, and the
hint never names a repository-local launcher to run); only HOME-rooted installs,
IMPECCABLE_BIN, the cache, and PATH entries outside the repo qualify, all by
realpath. The engine now sees a minimal environment (PATH, HOME, TMPDIR, locale,
IMPECCABLE_*), never the agent's tokens.
- `scan --changed <base>` pushed git-derived paths without the allow-list, so a
committed symlink with a frontend extension handed a file outside the repo to
the engine. Derived targets now go through the same allow-list as explicit ones
and symlinks named by git are refused outright.
- A repo-controlled `scripts/VERSION` with embedded newlines forged probe lines;
the version is trusted only when it is semver, and every printed version is
sanitized. Engine text containing the untrusted-content fence or a
`SENTINEL:` prefix is neutralized with a zero-width space
(neutralizeSentinels in the contract), so page text cannot close the envelope
or forge a probe line.
- A failing `git diff <base>...HEAD` (unknown or unfetched base) was swallowed
and read as "no frontend changes"; it is now DETECT_REFUSED with exit 1.
- The scan allow-list root follows `${GSTACK_HOME:-$HOME/.gstack}` like the
templates and gstack-slug (config.yaml keeps gstack-config's STATE_ROOT
precedence); a quoted or commented design_detector value reads correctly.
Smaller: raw engine chunks are kept only in --format raw; diagnostics are
capped (200 kept, 20 echoed); the engine identity hash reads size + 4 MB, not
the whole binary; PROBE_STEP and ENGINE_STDERR are contract sentinels; the
--verbose gate covers every probe step; analytics use one sentinel vocabulary;
bare limits live in DETECT_LIMITS. The fake engine's knobs are IMPECCABLE_FAKE_*
(so they pass the minimal env) and a shared test helper installs it. New tests
cover each item above plus clean runs, `{}` parse errors, missing paths, and the
50 MB stdout cap.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>