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
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,41 @@

<h1 align="center">version-sentinel</h1>

<p align="center">
<a href="https://github.com/KSEGIT/Version-Sentinel/releases/latest"><img src="https://img.shields.io/github/v/release/KSEGIT/Version-Sentinel?color=blue" alt="Release" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/github/license/KSEGIT/Version-Sentinel" alt="License: MIT" /></a>
<img src="https://img.shields.io/badge/Claude%20Code-plugin-8b5cf6" alt="Claude Code plugin" />
<img src="https://img.shields.io/badge/ecosystems-npm%20%7C%20pip%20%7C%20cargo%20%7C%20nuget-22c55e" alt="Supported ecosystems" />
</p>

<p align="center">Claude Code plugin that <strong>hard-blocks</strong> dependency additions, bumps, and downgrades until a fresh, source-cited version check is recorded.</p>

> 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 |
Expand Down Expand Up @@ -76,6 +103,28 @@ State: `<project-root>/.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)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## License

MIT — see [LICENSE](./LICENSE).
MIT — see [LICENSE](./LICENSE).
5 changes: 3 additions & 2 deletions scripts/detect-manifest-edit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ case "$tool_name" in
;;
MultiEdit)
post_content="$pre_content"
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
Expand Down Expand Up @@ -96,4 +97,4 @@ if [[ "$block" -eq 1 ]]; then
echo "$block_msgs" >&2
exit 2
fi
exit 0
exit 0
10 changes: 7 additions & 3 deletions scripts/lib/parse-install-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
parse_install_cmd() {
local cmd="$1"
local segment
while read -r segment; 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"
done < <(printf '%s\n' "$cmd" | tr ';&|' '\n')
done
}

_parse_install_segment() {
Expand Down Expand Up @@ -101,4 +105,4 @@ _emit_dotnet_add() {
esac
done
[[ -n "$name" ]] && printf 'csproj\t%s\t%s\n' "$name" "$ver"
}
}
Loading