Skip to content

Harden the pkgxray-guard install-gate (git-URL, fail-mode, cache, edit-diff)#18

Open
adamsjack711-ux wants to merge 7 commits into
CorridorSecurity:mainfrom
adamsjack711-ux:pkgxray-guard/harden-install-gate
Open

Harden the pkgxray-guard install-gate (git-URL, fail-mode, cache, edit-diff)#18
adamsjack711-ux wants to merge 7 commits into
CorridorSecurity:mainfrom
adamsjack711-ux:pkgxray-guard/harden-install-gate

Conversation

@adamsjack711-ux

Copy link
Copy Markdown

Follow-up hardening for the examples/pkgxray-guard install guard. main.go stays a thin shim; all logic lives in the stdlib-only pkgxrayguard package, and everything builds/tests in-tree with the root module (go build/vet/test ./examples/pkgxray-guard/..., 66 offline subtests).

Changes

  • Surface git/URL installs instead of dropping them. git+https:// / git@ / tarball-URL specs were silently skipped by the parser, so npm i git+https://… bypassed the gate. They now classify as an unvettable KindVCS spec that Guard short-circuits to REVIEW (pkgxray can't triage an arbitrary remote spec). Local file:/link:/workspace: paths are still skipped.
  • Shape-aware fail-mode. npx/bunx/pnpm dlx/bun x run package code the instant it resolves, with no persistent install to inspect. DecideResult escalates an immediate-exec spec whose verdict is UNKNOWN (pkgxray errored) or REVIEW to ask even under permissive — so it never fails open. Persistent installs still follow the policy table. DecideAll folds per-package results into the strongest action.
  • Finding location in messages. Deny/ask lines now include the file: [category] rationale (path).
  • Session memoization + cache passthrough. MemoGuard caches verdicts per exact ref@version for the process lifetime (each guard call is ~1.3–1.5s cold). Never caches UNKNOWN/errored results or across versions. PKGXRAY_CACHE_URL is forwarded to the CLI on cmd.Env.
  • Diff manifest edits. OnAfterFileEdit diffs the edit hunks: package.json deep-guards only newly added/changed deps (ManifestAddedSpecs, reusing the memo) — a formatting/script edit triages nothing; lockfiles keep the full pkgxray audit, which honors the sibling .pkgxray.lock allow/block memory. Falls back to a full audit when no added dep is extracted, so it's never less safe.

Notes

  • No changes outside examples/pkgxray-guard/; no new dependencies (stdlib only).
  • gofmt/go vet clean; the repo's go.yml exercises this on push.

🤖 Generated with Claude Code

adamsjack711-ux and others added 4 commits July 1, 2026 20:45
Guards AI-agent package installs with pkgxray before they run. Registers
OnBeforeExecution (parse npm/pnpm/yarn/bun installs + npx/bunx/pnpm-dlx
runners + `claude mcp add -- <launcher>`, run `pkgxray guard` per package,
deny on BLOCK with cited evidence) and an opt-in OnAfterFileEdit lockfile
audit. Policy layer (strict/balanced/permissive) fails closed on a broken
pkgxray under the default balanced policy.

Folded into the root module (no separate go.mod) so `go build/test ./...`
covers it, matching the multi-hook example. Core lives in a stdlib-only
pkgxrayguard package with offline table + fake-pkgxray exec tests.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-diff)

Follow-up hardening for the pkgxray × hookshot install guard. Keeps main a thin
shim; all logic stays in the stdlib-only pkgxrayguard package.

- Parser: git+https/git@/tarball-URL install specs were silently dropped (an
  `npm i git+https://…` sailed through). They now classify as an unvettable
  KindVCS spec that Guard short-circuits to REVIEW — surfaced, not allowed.
  Local file:/link:/workspace: paths are still skipped.
- Fail-mode is shape-aware: npx/bunx/pnpm-dlx/bun x run code the instant they
  resolve. DecideResult escalates an immediate-exec spec whose verdict is
  UNKNOWN (pkgxray errored) or REVIEW to ASK even under `permissive`, so it never
  fails open; persistent installs still follow the policy table. DecideAll folds
  per-package results into the strongest action.
- Deny/ask messages now include the finding's file path ([category] reason (file)).
- MemoGuard caches verdicts per exact ref@version for the process lifetime
  (~1.3–1.5s cold each); never caches UNKNOWN/errored results or across versions.
  PKGXRAY_CACHE_URL is forwarded to the CLI on cmd.Env.
- OnAfterFileEdit diffs the edit hunks: package.json deep-guards only the newly
  added/changed deps via ManifestAddedSpecs (reusing the memo); lockfiles keep
  the full `pkgxray audit`, which honors the sibling .pkgxray.lock memory. Falls
  back to a full audit when no added dep is extracted, so it's never less safe.

Builds/vets/tests green in-tree (`go build/vet/test ./examples/pkgxray-guard/...`);
66 offline pkgxrayguard subtests.
Two integration-completeness follow-ups:

- Document the pkgxray CLI contract the hook depends on (top-level `decision`,
  exit codes 2/3/0, `report.findings[].file`) and the minimum tested version
  (pkgxray >= 0.15.0), on the guardJSON type and in a README "Requirements"
  section. pkgxray has no `--version` flag, so there's no runtime probe; the
  hook instead degrades safely (missing/old/erroring pkgxray → UNKNOWN → denied
  under strict/balanced, never a false allow).
- Add ready-to-edit host configs for the two agents main.go already advertised
  but shipped no config for: Factory Droid (~/.factory/settings.json) and
  Windsurf Cascade (~/.codeium/windsurf/hooks.json). Values match hookshot's own
  installer (droid-pre-tool-use / droid-after-file-edit; cascade-pre-run-command
  / cascade-post-write-code). README layout now lists all five configs and the
  cache.go/manifest.go core files.
The unit tests drive the pkgxrayguard package against a fake pkgxray. e2e-smoke.sh
exercises the whole chain instead: it builds the hook binary and feeds it genuine
Claude PreToolUse payloads on stdin, so the compiled binary and the real pkgxray
CLI are actually integrated.

Deterministic cases (git-URL install → deny/ask, non-install → allow, local path
→ skipped) run offline and are hard assertions. One live case runs
`npm install left-pad` through the real pkgxray CLI and asserts a valid decision
comes back (verdict not pinned — it depends on the registry). Skips the live
case if pkgxray isn't on PATH. README "Try it" + layout updated.
Mirror of the pkgxray-side change: a command installing several packages guarded
them sequentially (~N × per-package pkgxray latency; a 20-pkg install ≈ 10s).

- pkgxrayguard.CheckAll: stdlib-only bounded fan-out (each Check writes its own
  result slot; MemoGuard mutex-guards its cache → concurrency-safe, go test -race
  clean). Used in decideInstalls + decideManifestSpecs. Cap via
  PKGXRAY_HOOK_CONCURRENCY (default 8). Results fold order-independently, so
  verdicts are unchanged.
- load-test.py: throughput/latency harness (offline + fake-pkgxray at high
  concurrency; real pkgxray at low volume). Measured one 20-pkg command at
  1607ms serial → 216ms (default 8) → 143ms (16).

Measured with go 1.26; builds/tests green in-tree.
The two exec sites — guard.go (`pkgxray guard <ref>`) and helpers.go
(`pkgxray audit <file>`) — tripped gosec G204 ("subprocess launched with a
variable"). Both are safe: exec.Command passes args as a []string argv (no
shell), so a package ref / path can't inject a command (CWE-78 is unreachable),
and `bin` is the operator-configured PKGXRAY_BIN, not attacker-controlled.

Close the findings with genuine defense-in-depth rather than bare suppression:

- guard.go: validateInstallRef() rejects an install ref that is empty, begins
  with '-' (argument injection into the pkgxray CLI), or carries control bytes —
  a sanitizer checkpoint on the untrusted-input → subprocess path. Registry refs
  the parser produces (name, @scope/name, name@version) always pass; a flag-like
  ref is surfaced as Review instead of being shelled out.
- Both exec sites carry a `#nosec G204` annotation documenting why the variable
  target is safe (no shell, operator-set bin, validated argv).

Verified: `gosec -include=G204 ./...` now reports 0 issues (2 nosec). Tests
cover validateInstallRef (valid refs pass, flag-like/control/empty refs error)
and that Check refuses a flag-like ref before exec. `go build`/`go vet`/
`go test ./...` all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adamsjack711-ux

Copy link
Copy Markdown
Author

On the helpers.go:25 finding:
false positive — G204 flags the variable, but there is no injection path (CWE-78 N/A):
exec.Command passes args as a []string argv, never a shell string, so a package name
or file path can't be reinterpreted as a command. bin is the operator-set PKGXRAY_BIN
(or PATH-resolved "pkgxray"), not attacker-controlled, and the args are literal
subcommands plus a manifest path the hook resolved itself. Spawning the pkgxray CLI is
this hook's intended function.

On the guard.go:97 finding:
false positive — same rationale as helpers.go. Args are a []string argv
("guard", , "--format", "json"), not a shell string, so spec.Ref is a single argv
element and cannot inject a command (no shell, no CWE-78). The ref is additionally
validated (validateInstallRef) and bin is operator-configured. Running the pkgxray
CLI is the core purpose of the gate.

adamsjack711-ux added a commit to adamsjack711-ux/hookshot that referenced this pull request Jul 4, 2026
…idorSecurity#18)

Brings the recheck branch up to date with the pkgxray source-of-truth
copy after the recheck + parallel-guard PRs merged there: adds
pkgxrayguard/batch.go + batch_test.go and the main.go dispatch changes.
Byte-identical per sync-to-hookshot.sh --check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant