From ddd735326ec8165ea3daf6e826cae2c306a7e91e Mon Sep 17 00:00:00 2001 From: DanyItNerd Date: Thu, 23 Apr 2026 11:01:32 +0100 Subject: [PATCH 1/3] docs(readme): add badges, keywords, comparison table, FAQ for SEO Boost discoverability: shields.io badges, explicit keyword line, "What it prevents" threat bullets, comparison vs Dependabot/audit skills, and 5-question FAQ covering Desktop support, overhead, private registries, vs npm audit, and roadmap ecosystems. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 89c8a91..09dd261 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,41 @@

version-sentinel

+

+ Release + License: MIT + Claude Code plugin + Supported ecosystems +

+

Claude Code plugin that hard-blocks dependency additions, bumps, and downgrades until a fresh, source-cited version check is recorded.

> If Claude tries to add `"lodash": "^4.17.21"` without looking up the latest version first, the tool call is rejected with exit 2. Claude must run `WebSearch`, then `/vs-record`, then retry. Five ecosystems supported in v0.1. +**Keywords:** Claude Code, Anthropic, AI coding guardrails, LLM supply-chain security, dependency management, hallucinated package versions, npm, PyPI, Cargo, NuGet, PreToolUse hook. + ## Why LLM-assisted coding silently ships whatever version the model remembers from its training data. For packages with frequent releases or known compromised versions, that's unacceptable. `version-sentinel` inserts a mandatory "check the registry" step — without stopping you from pinning an old version on purpose. +## What it prevents + +- **Hallucinated versions** — LLM picks a version that never existed or never shipped. +- **Stale defaults** — model reaches for a 2-year-old pin because training data froze there. +- **Compromised-release installs** — no guard against yanked / malicious versions without a fresh registry lookup. +- **Silent downgrades** — Claude "fixes" a CI error by reverting a package to an older vulnerable build. +- **Supply-chain drift** — no audit trail of *why* a specific version was chosen. + +## How it compares + +| Tool | Scope | Enforcement | +|------|-------|-------------| +| `version-sentinel` | Claude Code **PreToolUse hook** — blocks the tool call before the edit lands | Hard-fail exit 2 | +| Generic dependency-audit skills | Post-hoc scan of `package.json` / `requirements.txt` | Advisory | +| Dependabot / Renovate | Scheduled PR bot against remote registries | Async PR | + +Unlike post-hoc auditors, `version-sentinel` runs **inside the agent loop** — the agent cannot merge a bad version by accident because the write itself is refused until the check is cited. + ## Supported ecosystems (v0.1) | File | Ecosystem | Registry | @@ -76,6 +103,29 @@ State: `/.version-sentinel/checks.json`. Auto-gitignored on first /plugin marketplace remove version-sentinel-marketplace ``` +## FAQ + +**Does this work with Claude Desktop or just Claude Code?** +Claude Code only — relies on the PreToolUse hook API exposed by the CLI. + +**Does it slow Claude down?** +First touch of a package: adds one `WebSearch` + one `/vs-record` call (~5–10s). Subsequent edits to the same pin hit the cached sidecar — zero overhead. + +**Can I use this for private / internal registries?** +Yes — add the `ecosystem:pkg` entry to `.version-sentinel/ignore`, or record with a justification string. + +**Why not just run `npm audit` / `pip-audit`?** +Those are post-hoc. `version-sentinel` refuses the write in the first place, so the vulnerable version never enters the repo. + +**Does it support Go modules, Gradle, Maven, composer, gems?** +Not in v0.1. See `docs/roadmap.md`. + +## Related + +- [Anthropic Claude Code](https://claude.com/claude-code) +- [Claude Code plugin docs](https://code.claude.com/docs/en/plugin-dependencies) +- [Dependency-audit skills on buildwithclaude.com](https://www.buildwithclaude.com/command/dependency-audit) + ## License MIT — see [LICENSE](./LICENSE). From 0524f0ed0513a6cdca9f054d37838986dab95830 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:40:30 +0000 Subject: [PATCH 2/3] fix: apply CodeRabbit auto-fixes Fixed 3 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- README.md | 3 +-- scripts/detect-manifest-edit.sh | 6 ++++-- scripts/lib/parse-install-cmd.sh | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 09dd261..d3f660d 100644 --- a/README.md +++ b/README.md @@ -124,8 +124,7 @@ Not in v0.1. See `docs/roadmap.md`. - [Anthropic Claude Code](https://claude.com/claude-code) - [Claude Code plugin docs](https://code.claude.com/docs/en/plugin-dependencies) -- [Dependency-audit skills on buildwithclaude.com](https://www.buildwithclaude.com/command/dependency-audit) ## License -MIT — see [LICENSE](./LICENSE). +MIT — see [LICENSE](./LICENSE). \ No newline at end of file diff --git a/scripts/detect-manifest-edit.sh b/scripts/detect-manifest-edit.sh index b235275..b2254d4 100644 --- a/scripts/detect-manifest-edit.sh +++ b/scripts/detect-manifest-edit.sh @@ -54,12 +54,14 @@ case "$tool_name" in ;; MultiEdit) post_content="$pre_content" + local edits_tsv + edits_tsv=$(echo "$input" | jq -r '.tool_input.edits[]? | [.old_string, .new_string] | @tsv') while IFS=$'\t' read -r o n; do [[ -z "$o" ]] && continue o=$(printf '%s' "$o" | tr -d '\r') n=$(printf '%s' "$n" | tr -d '\r') post_content=$(printf '%s' "$post_content" | py_replace_once "$o" "$n") - done < <(echo "$input" | jq -r '.tool_input.edits[]? | [.old_string, .new_string] | @tsv') + done <<< "$edits_tsv" ;; *) exit 0 ;; esac @@ -96,4 +98,4 @@ if [[ "$block" -eq 1 ]]; then echo "$block_msgs" >&2 exit 2 fi -exit 0 +exit 0 \ No newline at end of file diff --git a/scripts/lib/parse-install-cmd.sh b/scripts/lib/parse-install-cmd.sh index b7d06a9..458154b 100644 --- a/scripts/lib/parse-install-cmd.sh +++ b/scripts/lib/parse-install-cmd.sh @@ -5,11 +5,12 @@ parse_install_cmd() { local cmd="$1" local segment - while read -r segment; do + local IFS=$'\n' + for segment in $(printf '%s\n' "$cmd" | tr ';&|' '\n'); do segment="${segment#"${segment%%[![:space:]]*}"}" [[ -z "$segment" ]] && continue _parse_install_segment "$segment" - done < <(printf '%s\n' "$cmd" | tr ';&|' '\n') + done } _parse_install_segment() { @@ -101,4 +102,4 @@ _emit_dotnet_add() { esac done [[ -n "$name" ]] && printf 'csproj\t%s\t%s\n' "$name" "$ver" -} +} \ No newline at end of file From ba2e07fd8f3e79eef25963f976a6241149952566 Mon Sep 17 00:00:00 2001 From: DanyItNerd Date: Tue, 28 Apr 2026 10:55:29 +0100 Subject: [PATCH 3/3] fix: IFS leak in parse_install_cmd and local outside function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_install_cmd set IFS=$'\n' which leaked into _emit_* helpers, preventing word-splitting on spaces — flags like --save-dev bled into version fields and dotnet --version wasn't parsed correctly. Remove stray `local` in detect-manifest-edit.sh case block (SC2168). Co-Authored-By: Claude Opus 4.6 --- scripts/detect-manifest-edit.sh | 1 - scripts/lib/parse-install-cmd.sh | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/detect-manifest-edit.sh b/scripts/detect-manifest-edit.sh index b2254d4..f1439b7 100644 --- a/scripts/detect-manifest-edit.sh +++ b/scripts/detect-manifest-edit.sh @@ -54,7 +54,6 @@ case "$tool_name" in ;; MultiEdit) post_content="$pre_content" - local edits_tsv edits_tsv=$(echo "$input" | jq -r '.tool_input.edits[]? | [.old_string, .new_string] | @tsv') while IFS=$'\t' read -r o n; do [[ -z "$o" ]] && continue diff --git a/scripts/lib/parse-install-cmd.sh b/scripts/lib/parse-install-cmd.sh index 458154b..1e842c1 100644 --- a/scripts/lib/parse-install-cmd.sh +++ b/scripts/lib/parse-install-cmd.sh @@ -5,8 +5,11 @@ parse_install_cmd() { local cmd="$1" local segment - local IFS=$'\n' - for segment in $(printf '%s\n' "$cmd" | tr ';&|' '\n'); do + local segments + local old_ifs="$IFS" + IFS=$'\n' read -r -d '' -a segments < <(printf '%s\n' "$cmd" | tr ';&|' '\n'; printf '\0') || true + IFS="$old_ifs" + for segment in "${segments[@]}"; do segment="${segment#"${segment%%[![:space:]]*}"}" [[ -z "$segment" ]] && continue _parse_install_segment "$segment"