Skip to content

feat(plugin): add Claude Code plugin v0.1.0#35

Open
dvcdsys wants to merge 9 commits intomainfrom
feat/claude-code-plugin
Open

feat(plugin): add Claude Code plugin v0.1.0#35
dvcdsys wants to merge 9 commits intomainfrom
feat/claude-code-plugin

Conversation

@dvcdsys
Copy link
Copy Markdown
Owner

@dvcdsys dvcdsys commented May 8, 2026

Summary

  • Ship cix as an installable Claude Code plugin (plugins/cix/) with bundled CLI auto-bootstrap, six /cix:* slash commands, lazy-loaded skill, and behavioral hooks (SessionStart + exponential-backoff nudge on Grep/Glob).
  • Repo doubles as a marketplace (.claude-plugin/marketplace.json) so users install with two commands; --sparse keeps checkout to ~50 KB.
  • New CLAUDE-CODE-PLUGIN.md covers prerequisites (server runs separately, CLI configured independently), install paths, verification, scope choice, uninstall, and troubleshooting.
  • README's Claude Code section now offers two integration paths — Option A "Plugin (recommended)" linking to CLAUDE-CODE-PLUGIN.md, Option B "Skill (manual, legacy)" preserved verbatim. The legacy skills/cix/SKILL.md and install.sh are unchanged.

Adoption design (why this layout)

Four-layer approach, total per-session overhead ~8 KB if cix is used heavily, ~400 B otherwise:

Layer Mechanism Cost
1. Skill description Native (always in context) ~200 B once
2. SessionStart hook One-time reminder if .cixignore present ~200 B once
3. PreToolUse(Grep|Glob) hook Nudge on call #1, 2, 4, 8, 16 … ~80 B × ~7
4. SKILL.md body Native lazy-load via skill mechanism ~7 KB once

The body is never duplicated — Claude Code's skill mechanism guarantees a single insertion that stays for the session.

What's NOT in this PR (deferred to v0.2)

  • MCP server exposing cix_search / cix_definitions / cix_references as native Claude tools (would unlock pure Claude Desktop chat usage).
  • PreToolUse(Bash) catching inline grep calls.
  • cix-explorer subagent.
  • release-plugin.yml workflow + plugin/v* tag stream — we'll wire CI after manual validation lands.

Files changed

  • .claude-plugin/marketplace.json — new marketplace catalog
  • CLAUDE-CODE-PLUGIN.md — new user-facing guide
  • README.md — Claude Code section refactored (plugin + skill)
  • plugins/cix/.claude-plugin/plugin.json — manifest
  • plugins/cix/skills/cix/SKILL.md — copy of legacy skill, tightened frontmatter
  • plugins/cix/commands/{search,def,refs,init,status,summary}.md — six slash commands
  • plugins/cix/hooks/hooks.json — SessionStart + PreToolUse(Grep|Glob)
  • plugins/cix/scripts/cix-wrapper.sh — use-system-or-install wrapper, exposed via bin/cix symlink
  • plugins/cix/scripts/session-start.sh — once-per-session reminder
  • plugins/cix/scripts/grep-nudge.sh — exponential-backoff nudge
  • plugins/cix/README.md — plugin-internal README

Existing files left untouched: install.sh, skills/cix/SKILL.md, cli/, server/, all CI workflows.

Test plan

  • claude plugin validate passes for both plugin and marketplace manifests
  • bash -n syntax check on all three hook scripts
  • Unit test grep-nudge.sh over 20 invocations — fires exactly on calls 1, 2, 4, 8, 16
  • Unit test session-start.sh — emits reminder when .cixignore exists, silent otherwise
  • cix-wrapper.sh exec's system cix when available, falls back to install.sh bootstrap when not
  • Local install via claude plugin install cix@code-index --scope user — plugin appears in claude plugin list, cache built at ~/.claude/plugins/cache/code-index/cix/0.1.0/ with symlink and chmod preserved
  • Manual end-to-end in Claude Code (CLI + Code mode in Claude Desktop) — install via marketplace, confirm slash autocomplete, verify SessionStart reminder appears in indexed projects, test auto-trigger on natural-language semantic prompts, confirm silent in projects without .cixignore
  • Verify GitHub-source install path: claude plugin marketplace add dvcdsys/code-index@feat/claude-code-plugin --sparse .claude-plugin plugins

Reviewer notes

  • The bin/cix is intentionally a symlink to ../scripts/cix-wrapper.sh. Claude Code's plugin cache preserves symlinks (verified locally). On install, the wrapper is exposed on PATH automatically per plugins-reference.md.
  • grep-nudge.sh uses the bit trick COUNT & (COUNT - 1) == 0 to detect powers of two. Counter file lives in /tmp/cix-grep-count-$SESSION_ID — zero-byte markers cleaned on /tmp reboot.
  • cix-wrapper.sh removes its own directory from PATH before searching for system cix, avoiding infinite recursion when plugin's bin/ is also on PATH.

🤖 Generated with Claude Code

dvcdsys and others added 6 commits May 8, 2026 22:34
Ship cix as an installable Claude Code plugin alongside the existing
manual skill, so users get one-command setup with bundled CLI, slash
commands, and behavioral hooks.

Plugin layout (plugins/cix/):
- skills/cix/SKILL.md — copy of the legacy skill with tightened
  description + when_to_use trigger phrases for reliable auto-trigger
- commands/{search,def,refs,init,status,summary}.md — 6 slash commands
  exposed as /cix:* with frontmatter + bash execution
- hooks/hooks.json — SessionStart + PreToolUse(Grep|Glob) registration
- scripts/cix-wrapper.sh — "use system cix or auto-install via the
  official install.sh" wrapper, exposed on PATH via bin/cix symlink
- scripts/session-start.sh — one-time reminder injected via
  hookSpecificOutput.additionalContext when project has .cixignore
- scripts/grep-nudge.sh — exponential-backoff nudge on Grep/Glob calls
  (fires on call #1, 2, 4, 8, 16, …; ~7 nudges per 100-Grep session)
- .claude-plugin/plugin.json — manifest, version 0.1.0

Marketplace (.claude-plugin/marketplace.json):
- Repo doubles as marketplace; one plugin entry pointing to plugins/cix/
- Compatible with `--sparse .claude-plugin plugins` so users skip the
  ~80 MB server/CLI/dashboard checkout

Docs:
- CLAUDE-CODE-PLUGIN.md (new) — full user-facing guide: prerequisites
  (server runs separately, CLI must be configured independently),
  install paths, verification, scope choice, uninstall, troubleshooting
- README.md — Claude Code section now exposes two integration paths:
  Option A "Plugin (recommended)" linking to CLAUDE-CODE-PLUGIN.md, and
  Option B "Skill (manual, legacy)" preserved verbatim for users who
  can't use the plugin system

Adoption design (4 layers, total context overhead ~8 KB per cix-heavy
session, ~400 B if cix isn't used):
1. Skill description (native, always-in-context)
2. SessionStart hook reminder (once per session)
3. PreToolUse(Grep|Glob) nudges (exponential backoff)
4. SKILL.md body lazy-loaded once via native skill mechanism

Validated with `claude plugin validate` for both plugin and marketplace
manifests; unit-tested hook scripts (backoff schedule, .cixignore
detection); installed locally via `claude plugin install` and verified
the wrapper resolves to system cix or bootstraps install.sh on demand.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tion

Previously hooks used the presence of `.cixignore` as a proxy for
"this project is cix-indexed". That's a weak signal — a project may be
registered with the cix-server without a `.cixignore` file (it's
optional, only needed for ignore patterns), and a stale `.cixignore`
can stick around after a project is removed from the server.

Switch SessionStart to ask `cix status` directly (~17 ms on a local
server, exit 0 only if the project is registered AND the server is
reachable). Cache the decision in `/tmp/cix-aware-$SESSION_ID` so
PreToolUse(Grep|Glob) can read it in ~1 ms without re-querying on
every Grep call.

session-start.sh:
- Resolves cix binary (plugin-bundled wrapper preferred, falls back to
  PATH lookup).
- Runs `cix status -p $CLAUDE_PROJECT_DIR` with a 2-second timeout
  implemented in pure bash (no coreutils dependency — macOS lacks
  `timeout`/`gtimeout` by default).
- Writes "1" or "0" to /tmp/cix-aware-$SESSION_ID.
- Injects the same SessionStart reminder on success, stays silent on
  failure (project not indexed, server unreachable, cix CLI missing).

grep-nudge.sh:
- Reads /tmp/cix-aware-$SESSION_ID first.
- Falls back to .cixignore presence if the cache is absent (covers
  resumed sessions where SessionStart didn't fire).
- Stays silent if neither source confirms cix-aware. No cix CLI calls.

Docs (CLAUDE-CODE-PLUGIN.md, plugins/cix/README.md):
- "How it works" / "Per-project trigger threshold" / "Hooks silent in
  indexed project" troubleshooting all reflect the new logic.
- Document the cache files (cix-aware + cix-grep-count) and their
  cleanup on /tmp reboot.

End-to-end test passed: SessionStart writes cache=1 in indexed repo,
cache=0 in /tmp; grep-nudge fires on calls 1, 2, 4, 8 reading the cache
in indexed repo, stays silent in /tmp.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…UGIN_DATA

Two changes addressing review feedback:

1. Move cache from /tmp to $CLAUDE_PLUGIN_DATA
   - /tmp on macOS is cleaned daily for files older than 3 days; on
     Linux it's typically cleared on reboot. Long-running Claude Code
     sessions outlived these windows, so the cache could vanish
     mid-session and force grep-nudge into its fallback path
     (now removed — see #2).
   - $CLAUDE_PLUGIN_DATA is plugin-persistent storage managed by Claude
     Code (resolves to ~/.claude/plugins/data/cix-code-index/), is
     exported to hook subprocesses, survives plugin updates, and is
     never cleaned by the OS.
   - SessionStart opportunistically deletes its own markers older than
     30 days so the directory doesn't grow unbounded.
   - Falls back to /tmp only when run outside a plugin context (tests).

2. Strict cache contract — no .cixignore fallback
   - Per user feedback: "if SessionStart concluded the project is not
     indexed, just turn off — don't pollute the agent with reminders".
   - grep-nudge previously fell back to checking .cixignore when the
     cache file was absent. That created false positives: a stale
     .cixignore left over from a removed project would trigger nudges
     even after `cix status` confirmed not-indexed.
   - New contract: cache file present + content "1" → nudges allowed;
     anything else (file missing, "0", or unreadable) → silent for the
     entire session.
   - Trade-off documented: a session that started before the cix-server
     came up stays silent even after the server recovers. Restart
     Claude Code to re-evaluate. Better to miss a few nudge opportunities
     than to nag a developer whose server is down.

Test coverage: end-to-end script tests pass for all three states —
indexed (NUDGE on calls 1, 2, 4, 8), not-indexed (silent), and
cache-absent (silent without .cixignore fallback).

Docs (CLAUDE-CODE-PLUGIN.md, plugins/cix/README.md):
- "How it works" section explains the strict cache policy.
- "Per-project trigger threshold" documents that a server reachable at
  SessionStart is required, and how to recover (restart session).
- "Hook state cleanup" section now points at $CLAUDE_PLUGIN_DATA and
  explains the 30-day GC.
- Troubleshooting "Hooks silent in indexed project" lists the inspection
  command for the cache file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review feedback: $CLAUDE_PLUGIN_DATA isn't cleaned by the OS, so we
need to remove our own markers. Add a SessionEnd hook that deletes the
two per-session files when a session terminates normally.

scripts/session-end.sh:
- Reads session_id from stdin JSON.
- Removes $CLAUDE_PLUGIN_DATA/cix-aware-$SESSION_ID and
  $CLAUDE_PLUGIN_DATA/cix-grep-count-$SESSION_ID.
- Best-effort: failures silently ignored, since SessionEnd's last word
  shouldn't include error noise.

hooks/hooks.json:
- Register SessionEnd alongside the existing SessionStart and
  PreToolUse(Grep|Glob) entries.

Two-tier cleanup strategy now in place:
1. SessionEnd handles the normal exit path.
2. 30-day GC in SessionStart covers forced kills (kill -9, OOM, panic)
   where SessionEnd never runs.

End-to-end lifecycle test passed: SessionStart writes cache (1 byte),
grep-nudge increments counter (NUDGE 1, 2, 4 / silent 3), SessionEnd
removes both files leaving the cache dir empty.

Docs updated (CLAUDE-CODE-PLUGIN.md, plugins/cix/README.md): explain
the two-tier cleanup and document SessionEnd alongside the other hooks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cache

Two new hooks address gaps that surfaced during design review:

CwdChanged
  - When Claude changes working directory mid-session (`cd ../other`),
    the existing cache (`cix-aware-$SESSION_ID`) was stale — it held
    the verdict for the original project, not the new one. The hook
    now caches a separate verdict per (session, project_dir) so a
    multi-project flow gets correct cix-awareness in each.
  - Behavior: silent on cd (no reminder injection) — PreToolUse(Grep|Glob)
    handles the first-Grep-in-new-project nudge through its per-project
    backoff counter, which resets per directory.
  - No-op if the new directory was already evaluated in this session
    (Claude bouncing back to a known project).

PostCompact
  - Skill bodies survive auto-compaction natively (re-attached up to
    5K tokens per skill, see Claude Code skills docs). But the
    SessionStart `additionalContext` reminder — and PreToolUse nudges
    — are NOT skills; they're tool result messages and get dropped or
    summarised during compaction.
  - In long sessions (8+ hours) where the cix skill hasn't been
    invoked yet, the model can "forget" cix exists after compaction.
    PostCompact re-injects the SessionStart reminder if the current
    project's cache says "1".
  - No-op if cache is "0" or missing.

Cache key change (breaking for in-flight sessions; benign for new ones)
  - Old: $CLAUDE_PLUGIN_DATA/cix-aware-$SESSION_ID
  - New: $CLAUDE_PLUGIN_DATA/cix-aware-$SESSION_ID-$DIR_HASH
    where DIR_HASH = `shasum -a 256 <<< $PROJECT_DIR | cut -c1-8`
    (works on both macOS and Linux; falls back to a path-derived
    suffix if shasum is missing).
  - Per-project state means each directory has its own backoff counter:
    a fresh `cd` into a cix-aware project always fires a nudge on the
    first Grep, not the 32nd.

session-start.sh, grep-nudge.sh: refactored to compute DIR_HASH inline
  and use the new key. Same algorithm in both; bash sourcing avoided
  to keep each script standalone-runnable for testing.

session-end.sh: switched from `rm -f $cix-aware-$SID $cix-grep-count-$SID`
  to a glob `find -name 'cix-aware-$SID-*' -delete` so it cleans every
  per-(session, dir) marker the session created.

hooks.json: register CwdChanged and PostCompact alongside SessionStart,
  PreToolUse(Grep|Glob), and SessionEnd. Five hooks total now.

doc/TODO.md (new): document `PostToolUseFailure` for `Bash(cix *)` as a
  v0.2 deferred item. Original ask was an interactive UI prompt
  ("Disable cix plugin for this session? Yes/No"); investigation showed
  Claude Code's hook API doesn't support arbitrary user dialogs
  (`permissionDecision: "ask"` is PreToolUse-only). The functional
  equivalent — overwrite cache to "0" + inject explanation message —
  is straightforward but the underlying problem (cix-server flapping
  mid-session) is rare enough that we want real-usage data before
  shipping. ~1 day of work, queued.

Test coverage: 10-step multi-dir integration test exercises the full
lifecycle — SessionStart, Greps with backoff, CwdChanged into non-cix
dir, silent Greps there, CwdChanged back (preserves counter), more
Greps continuing the original backoff, PostCompact re-injection,
PostCompact silent for non-cix dir, 3 cache files persisted,
SessionEnd glob cleans everything.

Docs (CLAUDE-CODE-PLUGIN.md, plugins/cix/README.md): "Behavioral hooks"
sections enumerate all 5 hooks, explain the per-(session, dir) cache
key, and document the strict cache contract for each.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address security concern: bash hook scripts run on every Claude Code
session and call `find -delete` against $CLAUDE_PLUGIN_DATA. Without
tests and without runtime guards, a misconfigured environment could in
principle wipe arbitrary files. Layered defense:

1. Path validation guards (runtime, in scripts)
   session-start.sh and session-end.sh now refuse to operate if
   $CLAUDE_PLUGIN_DATA is outside the whitelist:
     - $HOME/.claude/plugins/data/*  (official plugin-data dir)
     - /tmp or /tmp/*                 (fallback / installer scratch)
     - $TMPDIR/*                      (macOS test sandboxes)
   Anything else (e.g. /, $HOME, /etc) prints a refusal message and
   exits non-zero without touching anything.

2. Conservative find patterns (already in place, hardened)
   Every find -delete uses -maxdepth 1, -type f, and a tight -name
   filter. No rm -rf anywhere in the plugin. Subdirectories, symlinks,
   and files outside the cix-aware-* / cix-grep-count-* patterns are
   never reached.

3. bats-core test suite (plugins/cix/tests/)
   46 tests across 6 files, all passing on macOS. Covers:
     - session-start.bats — cix-status flow, cache write, GC,
       guard refusals (=, =$HOME, =/etc), guard accepts (plugin-data,
       /tmp, $TMPDIR)
     - cwd-changed.bats — first-cd evaluation, no-op on cached dir,
       multi-dir cache, silent on cd, timeout
     - grep-nudge.bats — exponential backoff (1, 2, 4, 8, 16),
       per-(session, dir) counters, no cix CLI calls (cache-only)
     - post-compact.bats — re-injection only when cache="1"
     - session-end.bats — glob deletion across dir hashes,
       SECURITY: never touches other sessions' files, never touches
       non-cix files even if they look similar, GUARD refuses on /
       and $HOME and /etc, SECURITY: shell-injection in session_id
       does not delete a /tmp canary
     - cix-wrapper.bats — system-cix passthrough, exit code propagation,
       arg propagation, self-recursion guard via symlink chain

   Mocks: tests/mocks/bin/cix is a fake CLI controlled via env vars
   (MOCK_CIX_EXIT, MOCK_CIX_DELAY, MOCK_CIX_LOG_FILE). helpers.bash
   provides setup_test_env / teardown_test_env / run_hook / make_cache /
   read_cache / read_counter / compute_hash / mock_cix_call_count.

4. GitHub Actions workflow (.github/workflows/ci-plugin.yml)
   Runs on push and PR when plugins/cix/** or .claude-plugin/** changes.
   Matrix: ubuntu-latest + macos-latest. Steps:
     - Install bats-core, jq, shellcheck (apt or brew)
     - Run bats --tap plugins/cix/tests/*.bats
     - Run shellcheck --severity=warning on all hook scripts
     - Validate JSON manifests (marketplace, plugin, hooks)
     - Verify bin/cix symlink integrity
   Fails on shellcheck warnings, not just errors — keeps scripts
   defensive about quoting, word splitting, and unsafe globs.

5. Documentation
   plugins/cix/tests/README.md — how to run locally, test matrix
   summary, mock interface, how to add new tests.
   CLAUDE-CODE-PLUGIN.md — new "Security & testing" section
   documenting the three defensive layers.

All 46 tests pass locally on macOS 14 with bats-core 1.13.0.
shellcheck --severity=warning is clean on all 6 hook scripts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread .github/workflows/ci-plugin.yml Fixed
dvcdsys and others added 3 commits May 8, 2026 23:25
…instead

Previous commit added `case "$CACHE_DIR" in $HOME/.claude/plugins/data/*|/tmp*|...) ;;
*) refuse;;` guards to session-start.sh and session-end.sh. Per review:
the whitelist blocks legitimate non-standard layouts (corporate setups
with custom CLAUDE_PLUGIN_DATA, XDG-style layouts, alternative paths)
while providing no real safety advantage on top of the existing
file-level filters.

Real safety model: every `find -delete` already uses
  -maxdepth 1   (no recursion)
  -type f       (files only — skips dirs/symlinks)
  -name 'cix-aware-...' / -name 'cix-grep-count-...'  (exact prefix)

For session-end.sh the patterns also embed $SESSION_ID (a Claude-Code-
assigned UUID), so the patterns practically cannot match anything but
our own marker files even in unusual cache dirs. There's no path on
which a hook script could touch a file that doesn't already match the
strict name pattern.

Removed:
- session-start.sh: path whitelist case-block before GC
- session-end.sh: path whitelist case-block before find -delete
- 5 tests in session-start.bats: GUARD: refuses /, $HOME, /etc;
  GUARD: allows plugin-data; GUARD: allows /tmp
- 3 tests in session-end.bats: GUARD: refuses /, $HOME, /etc

Added:
- session-start.bats: "GC never deletes files outside the cix-aware-*
  / cix-grep-count-* prefixes — even in same dir" — populates cache
  dir with confusable names (cix-other-pattern, X-cix-aware-fake-...,
  cix alone, AWARE-MISTAKEN-CASE) all 90 days old, asserts every one
  survives GC.
- session-end.bats: "in a non-standard cache dir, only matching files
  are deleted" — uses a fresh mktemp dir under BATS_TMPDIR (NOT in
  the old whitelist), populates with secrets.json, kubeconfig.yaml,
  .env, deploy.sh + a subdirectory with a nested file, asserts only
  cix-aware-our-sess-* and cix-grep-count-our-sess-* are deleted.

Test count: 46 → 41 (5 guard tests removed, 2 positive tests added,
8 - 3 net). All 41 pass on macOS 14 + bats 1.13.0. shellcheck still
clean.

Docs (CLAUDE-CODE-PLUGIN.md, plugins/cix/tests/README.md): "Security
& testing" section now explains the file-level safety model and
explicitly documents that custom data dirs are supported.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CodeQL flagged ci-plugin.yml on the PR with: "Workflow does not contain
permissions". The workflow only reads repo contents (clones, runs
tests, validates manifests) — it never pushes commits, opens PRs,
posts comments, or touches releases. Lock GITHUB_TOKEN down to:

    permissions:
      contents: read

at the workflow level so the implicit default (which can be repo-wide
write on legacy installs) is overridden everywhere.

Also: document the pre-existing govulncheck failure on `main` in
doc/TODO.md. Two Go stdlib vulnerabilities (GO-2026-4971, GO-2026-4918)
landed in 1.25.10; server/go.mod still pins 1.25.9. Bumping the
go directive is a 5-minute fix in a separate PR — outside this
plugin's scope, so flagged for the server roadmap rather than handled
here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
govulncheck flagged two CVEs reachable from server code:

- GO-2026-4971 — Panic in net.Dial and net.LookupPort when handling
  NUL byte on Windows. Reachable from
  internal/embeddings/client.go:240 (http.Client.Do →
  net.Dialer.DialContext) and internal/embeddings/supervisor.go:158
  (net.Listen for free-port picking).

- GO-2026-4918 — Infinite loop in HTTP/2 transport when given bad
  SETTINGS_MAX_FRAME_SIZE in golang.org/x/net/http2. Reachable from
  internal/embeddings/client.go:240 (http.Client.Do) and
  cmd/cix-server/main.go:38 (healthcheck http.Client.Get).

Both fixed in go1.25.10. Bump the `go` directive in server/go.mod;
go.sum is unchanged (no dependency-tree changes from a stdlib version
bump).

CI uses `go-version-file: server/go.mod` so this is the only change
needed — the workflow will pick up 1.25.10 on the next run.

Verified locally with GOTOOLCHAIN=go1.25.10:
  - go build ./...        — clean
  - govulncheck ./...     — "No vulnerabilities found."

Reachability:
  - GO-2026-4971 — Windows-only code path; Linux/macOS deployments
    weren't affected at runtime, but govulncheck flagged the call site.
  - GO-2026-4918 — HTTP/2 transport; real risk on any cix-server
    instance reachable by an untrusted peer. This is the one that
    actually mattered.

Removed the corresponding entry from doc/TODO.md (no longer pending).

Co-Authored-By: Claude Opus 4.7 <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.

2 participants