@softspark/ai-toolkit 4.4.2 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,39 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.5.1 - ShellCheck fix for loop-guard hook (2026-06-02)
11
+
12
+ Patch release. Clears two ShellCheck `SC2034` warnings in `loop-guard.sh` that turned the `main` CI ShellCheck job red after v4.5.0. The publish workflow does not run ShellCheck, so v4.5.0 published despite the red CI; this patch makes `main` green again. No runtime behavior change.
13
+
14
+ ### Fixed
15
+ - **`loop-guard.sh` ShellCheck** — `INPUT` now carries the `# shellcheck disable=SC2034` directive (it is consumed via the sourced `_hook-io.sh`, matching `guard-destructive.sh`), and `AI_TOOLKIT_HOOK_FORMAT` is `export`ed. `shellcheck --severity=warning app/hooks/*.sh` is clean.
16
+
17
+ ---
18
+
19
+ ## v4.5.0 - Unicode-safety scanner, loop guard, honest instincts (2026-06-02)
20
+
21
+ Minor release. Hardens the security audit against invisible prompt injection, adds a repeated-action loop guard, fixes destructive-guard false positives, and turns the instinct system from a dormant promise into a working manual feature.
22
+
23
+ ### Added
24
+ - **Unicode-safety scanner** — `scripts/audit_skills.py` now flags invisible/smuggled Unicode across shipped prompt text (skills, agents, rules, personas, mcp-templates): tag-block ASCII smuggling (`U+E0000–E007F`) and Trojan Source bidi controls as HIGH, zero-width/invisible format chars as WARN. Emoji ZWJ (`U+200D`) is allowlisted. Runs inside the existing `--ci` gate.
25
+ - **`loop-guard.sh`** — advisory `PostToolUse` hook (`Bash|Edit|MultiEdit|Write`) that warns when the same action repeats within a short window, catching successful-but-identical loops the `/repeat` circuit breaker (failures-only) misses. Edits track content, so iterative editing of one file does not trip it. Tunable via `AI_TOOLKIT_LOOP_WINDOW`/`AI_TOOLKIT_LOOP_THRESHOLD`. Hook entries: 28 → 29.
26
+ - **Per-hook opt-out** — `AI_TOOLKIT_DISABLED_HOOKS` (comma list) disables named profile-gated hooks via `_profile-check.sh`. Safety guards are intentionally excluded — remove them with `ai-toolkit remove-hook`.
27
+ - **Editor hooks honesty** — the README platform matrix gained a **Hooks** column marking which editors get lifecycle hook enforcement; `validate.py` cross-checks the claim against the actual `generate_*_hooks.py` generators and fails on drift.
28
+ - **`doctor` language-rules drift check** — warns when a local-install project gained a language (e.g. a new `Cargo.toml`) whose `<lang>-rules` were never injected.
29
+ - **Live-app rubric verification** — `verification-before-completion` documents an optional weighted-rubric scoring pass for behavioral/visual work.
30
+
31
+ ### Changed
32
+ - **Instincts load by default** — `session-start.sh` loads `.claude/instincts/*.md` whenever present (previously gated behind `AI_TOOLKIT_HOOK_VERBOSE=1`, so they never surfaced by default). `AI_TOOLKIT_HOOK_QUIET=1` suppresses; no instincts on disk means no output.
33
+ - **memory-pack `strip_private.py`** — now redacts high-confidence secrets (API keys, tokens, private keys) to `[REDACTED:<label>]` in addition to stripping `<private>` blocks before SQLite storage.
34
+
35
+ ### Fixed
36
+ - **`guard-destructive.sh` false positives** — `git push --force-with-lease`/`--force-if-includes` are no longer blocked, and a single non-chained `echo`/`printf`/`git commit`/`git tag` carrying a destructive token as data (e.g. a commit message mentioning `DROP TABLE`) passes. Chained commands (`&&`, `||`, `;`, `|`) and real executions (e.g. `psql -c "DROP TABLE"`) are still inspected and blocked.
37
+ - **`instinct-review` documentation** — removed the false claim of an automatic session-end instinct extractor that never existed; the skill now documents the real manual authoring + curation flow.
38
+
39
+ Test count: 1151 → 1179. No skill/agent count change.
40
+
41
+ ---
42
+
10
43
  ## v4.4.2 - Ecosystem sync: Claude Code 2.1.158 + Codex 0.134 hook events (2026-05-30)
11
44
 
12
45
  Patch release. Syncs the tool registry and `hook-creator` skill with newly detected upstream hook events and refreshes the ecosystem drift snapshot. No generated output or runtime behavior changes.
package/README.md CHANGED
@@ -6,16 +6,17 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![Skills](https://img.shields.io/badge/skills-107-brightgreen)](app/skills/)
8
8
  [![Agents](https://img.shields.io/badge/agents-44-blue)](app/agents/)
9
- [![Tests](https://img.shields.io/badge/tests-1151%20passing-success)](tests/)
9
+ [![Tests](https://img.shields.io/badge/tests-1179%20passing-success)](tests/)
10
10
 
11
- ## What's New in v4.4.2
11
+ ## What's New in v4.5.1
12
12
 
13
- Patch release. Syncs the tool registry and `hook-creator` skill with newly detected upstream hook events and refreshes the ecosystem drift snapshot. No generated output or runtime behavior changes.
13
+ v4.5.1 is a patch that clears a ShellCheck lint in the loop-guard hook. The v4.5.0 feature set: hardens the security audit against invisible prompt injection, adds a repeated-action loop guard, fixes destructive-guard false positives, and makes the instinct system actually work.
14
14
 
15
- - **Claude Code `MessageDisplay`**: the hook event added in Claude Code 2.1.152 is now tracked in the registry and documented in the `hook-creator` skill.
16
- - **Three more Claude Code events tracked**: `PostToolUseFailure`, `PostToolBatch`, and `UserPromptExpansion` were real but missing from the registry.
17
- - **Codex hook events**: recorded the four untracked Codex events (`PreCompact`, `PostCompact`, `SubagentStart`, `SubagentStop`); Codex exposes 10 lifecycle events total.
18
- - **Drift snapshot refreshed**: baselined Claude Code 2.1.158 and Codex 0.134; `ecosystem_doctor.py --check` is green.
15
+ - **Unicode-safety scanner**: the skill/agent audit now flags invisible and smuggled Unicode (tag-block ASCII smuggling, Trojan Source bidi controls) across all shipped prompt text — a prompt-injection vector regex checks never caught. Runs in the `--ci` gate.
16
+ - **Loop guard**: a new advisory `PostToolUse` hook warns when the same action repeats, catching stuck loops that the `/repeat` failure-counter circuit breaker misses.
17
+ - **`git push --force-with-lease` unblocked**: the destructive-command guard no longer false-positives on safe force-pushes or on commit messages that merely mention `DROP TABLE`/`rm -rf`.
18
+ - **Instincts load by default**: hand-authored `.claude/instincts/*.md` now load at session start (previously dormant behind a verbose flag). The skill docs were corrected to drop a never-implemented auto-extractor claim.
19
+ - **Editor honesty + secret scrubbing**: the platform matrix marks which editors get hook enforcement, and memory-pack now redacts secrets before storing session observations.
19
20
 
20
21
  See [CHANGELOG.md](CHANGELOG.md) for full history.
21
22
 
@@ -98,22 +99,22 @@ See [CLI Reference](kb/reference/cli-reference.md) for all commands and options.
98
99
 
99
100
  ## Platform Support
100
101
 
101
- | Platform | Config Files | Scope |
102
- |----------|-------------|-------|
103
- | Claude Code | `~/.claude/` | global |
104
- | Cursor | `.cursor/rules/*.mdc` + `.cursor/mcp.json` + `.cursor/skills/*` | project (`~/.cursor/mcp.json` for MCP only) |
105
- | Windsurf | `~/.codeium/.../global_rules.md` + `~/.codeium/windsurf/skills/*` + `.windsurf/rules/*.md` | global + project |
106
- | Gemini CLI | `~/.gemini/GEMINI.md` | global |
107
- | GitHub Copilot | `.github/copilot-instructions.md` | project |
108
- | Cline | `~/.cline/rules/*.md` + `~/.cline/skills/*` + `.clinerules/*.md` | global + project |
109
- | Roo Code | `~/.roo/rules/*.md` + `.roomodes` + `.roo/rules/*.md` | global rules + project |
110
- | Aider | `~/.aider.conf.yml` + `.aider.conf.yml` + `CONVENTIONS.md` | global + project |
111
- | Augment | `~/.augment/rules/*.md` + `.augment/rules/ai-toolkit-*.md` | global + project |
112
- | Google Antigravity | `.agent/rules/*.md` + `.agent/workflows/*.md` | project |
113
- | Codex CLI | `AGENTS.md` + `.agents/rules/*.md` + `.agents/skills/*` + `.codex/hooks.json` | project + global plugin |
114
- | opencode | `AGENTS.md` + `.opencode/{agents,commands,plugins}/*` + `opencode.json` | project + global (`~/.config/opencode/`) |
115
-
116
- > Claude Code is always installed (primary platform). Other editors on demand with `--editors`. All platforms receive the same agent/skill catalog, guidelines, and registered custom rules.
102
+ | Platform | Config Files | Hooks | Scope |
103
+ |----------|-------------|:-----:|-------|
104
+ | Claude Code | `~/.claude/` | ✅ | global |
105
+ | Cursor | `.cursor/rules/*.mdc` + `.cursor/mcp.json` + `.cursor/skills/*` | ✅ | project (`~/.cursor/mcp.json` for MCP only) |
106
+ | Windsurf | `~/.codeium/.../global_rules.md` + `~/.codeium/windsurf/skills/*` + `.windsurf/rules/*.md` | ✅ | global + project |
107
+ | Gemini CLI | `~/.gemini/GEMINI.md` | ✅ | global |
108
+ | GitHub Copilot | `.github/copilot-instructions.md` | — | project |
109
+ | Cline | `~/.cline/rules/*.md` + `~/.cline/skills/*` + `.clinerules/*.md` | — | global + project |
110
+ | Roo Code | `~/.roo/rules/*.md` + `.roomodes` + `.roo/rules/*.md` | — | global rules + project |
111
+ | Aider | `~/.aider.conf.yml` + `.aider.conf.yml` + `CONVENTIONS.md` | — | global + project |
112
+ | Augment | `~/.augment/rules/*.md` + `.augment/rules/ai-toolkit-*.md` | ✅ | global + project |
113
+ | Google Antigravity | `.agent/rules/*.md` + `.agent/workflows/*.md` | — | project |
114
+ | Codex CLI | `AGENTS.md` + `.agents/rules/*.md` + `.agents/skills/*` + `.codex/hooks.json` | ✅ | project + global plugin |
115
+ | opencode | `AGENTS.md` + `.opencode/{agents,commands,plugins}/*` + `opencode.json` | ✅ | project + global (`~/.config/opencode/`) |
116
+
117
+ > Claude Code is always installed (primary platform). Other editors on demand with `--editors`. Every platform receives the agent/skill catalog, guidelines, and registered custom rules as text. The **Hooks** column marks platforms that also get lifecycle hook enforcement — the machine-enforced constitution (guard-destructive, quality gates, search-first discipline). Platforms marked — receive those rules as guidance only, without blocking hooks.
117
118
 
118
119
  ---
119
120
 
@@ -125,7 +126,7 @@ See [CLI Reference](kb/reference/cli-reference.md) for all commands and options.
125
126
  | `skills/` (hybrid) | 30 | Slash commands with agent knowledge base |
126
127
  | `skills/` (knowledge) | 45 | Domain knowledge auto-loaded by agents (includes 13 `<lang>-rules` skills) |
127
128
  | `agents/` | 44 | Specialized agents across 10 categories |
128
- | `hooks/` | 28 entries / 14 events | Quality gates, path safety, prompt governance, session lifecycle |
129
+ | `hooks/` | 29 entries / 14 events | Quality gates, path safety, prompt governance, loop guard, session lifecycle |
129
130
  | `plugins/` | 11 packs | Opt-in domain bundles (security, research, frontend, enterprise, 6 language packs) |
130
131
  | `constitution.md` | 6 articles | Machine-enforced safety rules |
131
132
  | `rules/` | auto-injected | Language-specific and custom rules injected into your configs |
@@ -141,14 +142,14 @@ ai-toolkit/
141
142
  │ ├── agents/ # 44 agent definitions
142
143
  │ ├── skills/ # 107 skills (task / hybrid / knowledge)
143
144
  │ ├── rules/ # Auto-injected into your CLAUDE.md
144
- │ ├── hooks/ # Hook scripts (28 entries, 14 lifecycle events)
145
+ │ ├── hooks/ # Hook scripts (29 entries, 14 lifecycle events)
145
146
  │ ├── plugins/ # 11 experimental plugin packs (opt-in)
146
147
  │ ├── output-styles/ # System prompt output style overrides
147
148
  │ ├── constitution.md # 6 immutable safety articles
148
149
  │ └── ARCHITECTURE.md # Full system design
149
150
  ├── kb/ # Reference docs, procedures, plans
150
151
  ├── scripts/ # Validation, install, evaluation scripts
151
- ├── tests/ # Bats test suite (1151 tests)
152
+ ├── tests/ # Bats test suite (1179 tests)
152
153
  └── CHANGELOG.md
153
154
  ```
154
155
 
@@ -160,7 +161,7 @@ ai-toolkit/
160
161
 
161
162
  **Machine-enforced constitution** — 6-article safety constitution enforced via `PreToolUse` hooks that actually block `rm -rf`, `DROP TABLE`, and irreversible operations. Not just documentation.
162
163
 
163
- **28 lifecycle hooks** — Executable scripts across 14 events (SessionStart → SessionEnd, plus InstructionsLoaded + ConfigChange). Guards, governance, quality gates, session persistence, MCP health checks, revert protection, test-cohesion enforcement, search-first discipline. See [Hooks Catalog](kb/reference/hooks-catalog.md).
164
+ **29 lifecycle hooks** — Executable scripts across 14 events (SessionStart → SessionEnd, plus InstructionsLoaded + ConfigChange). Guards, governance, quality gates, session persistence, MCP health checks, revert protection, test-cohesion enforcement, loop guard, search-first discipline. See [Hooks Catalog](kb/reference/hooks-catalog.md).
164
165
 
165
166
  **Security scanning** — `/skill-audit` for code-level risks, `/cve-scan` for dependency CVEs. Both CI-ready with exit codes.
166
167
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-toolkit",
3
3
  "description": "Professional-grade Claude Code toolkit with persona presets, skill security auditor, expanded lifecycle hooks, experimental opt-in plugin packs, benchmark harvesting, and multi-tool support.",
4
- "version": "4.4.2",
4
+ "version": "4.5.1",
5
5
  "author": {
6
6
  "name": "SoftSpark",
7
7
  "url": "https://github.com/softspark"
@@ -8,7 +8,7 @@ Universal multi-agent system for software development. Works across all reposito
8
8
  |-----------|-------|
9
9
  | Agents | See agents catalog |
10
10
  | Skills | See skills catalog |
11
- | Hooks | 14 events / 28 entries (SessionStart ×3, Notification ×1, PreToolUse ×5, UserPromptSubmit ×2, PostToolUse ×4, Stop ×4, TaskCompleted ×1, TeammateIdle ×1, SubagentStart ×1, SubagentStop ×1, PreCompact ×2, SessionEnd ×1, InstructionsLoaded ×1, ConfigChange ×1) |
11
+ | Hooks | 14 events / 29 entries (SessionStart ×3, Notification ×1, PreToolUse ×5, UserPromptSubmit ×2, PostToolUse ×5, Stop ×4, TaskCompleted ×1, TeammateIdle ×1, SubagentStart ×1, SubagentStop ×1, PreCompact ×2, SessionEnd ×1, InstructionsLoaded ×1, ConfigChange ×1) |
12
12
 
13
13
  ---
14
14
 
@@ -9,3 +9,20 @@
9
9
 
10
10
  PROFILE="${TOOLKIT_HOOK_PROFILE:-standard}"
11
11
  [ "$PROFILE" = "minimal" ] && exit 0
12
+
13
+ # Per-hook soft opt-out: AI_TOOLKIT_DISABLED_HOOKS is a comma-separated list of
14
+ # hook names (with or without the .sh suffix). A listed hook exits 0 (no-op).
15
+ # Only hooks that source this file are covered — the safety guards
16
+ # (guard-destructive/path/config) intentionally do not, and must be removed
17
+ # deliberately with `ai-toolkit remove-hook` rather than silently env-disabled.
18
+ if [ -n "${AI_TOOLKIT_DISABLED_HOOKS:-}" ]; then
19
+ _hook_name="$(basename "${BASH_SOURCE[1]:-$0}" .sh)"
20
+ IFS=',' read -ra _disabled_hooks <<< "$AI_TOOLKIT_DISABLED_HOOKS"
21
+ for _d in "${_disabled_hooks[@]}"; do
22
+ _d="${_d// /}"
23
+ _d="${_d%.sh}"
24
+ if [ -n "$_d" ] && [ "$_d" = "$_hook_name" ]; then
25
+ exit 0
26
+ fi
27
+ done
28
+ fi
@@ -18,6 +18,24 @@ fi
18
18
  # Normalize command for matching: collapse whitespace, strip backslash escapes
19
19
  NORMALIZED=$(printf '%s' "$COMMAND" | tr -s '[:space:]' ' ' | sed 's/\\//g')
20
20
 
21
+ # Benign data contexts: a single, non-chained command whose program only PRINTS
22
+ # or RECORDS text (echo/printf) or writes a commit/tag message (git commit/tag)
23
+ # cannot itself execute a destructive op — the dangerous tokens are data, not
24
+ # actions. Skip matching so "git commit -m 'fix DROP TABLE race'" is not blocked.
25
+ # Bail out of the allowlist when a command separator is present (&&, ||, ;, |) so
26
+ # chained commands like `git commit -m x && rm -rf /tmp` are still inspected.
27
+ if ! printf '%s' "$NORMALIZED" | grep -qE '(&&|\|\||;|\|)'; then
28
+ if printf '%s' "$NORMALIZED" | grep -qE '^[[:space:]]*(echo|printf|git[[:space:]]+(commit|tag))([[:space:]]|$)'; then
29
+ exit 0
30
+ fi
31
+ fi
32
+
33
+ # Safe git force variants (--force-with-lease / --force-if-includes) are the
34
+ # recommended way to force-push without clobbering others' work. Strip them
35
+ # before matching so they do not trip the broad --force patterns; a bare
36
+ # --force or -f left behind still blocks.
37
+ SAFE_STRIPPED=$(printf '%s' "$NORMALIZED" | sed -E 's/--force-with-lease(=[^ ]*)?//g; s/--force-if-includes//g')
38
+
21
39
  # Destructive patterns — word-boundary aware where possible
22
40
  DESTRUCTIVE_PATTERNS=(
23
41
  # rm variants (short flags, long flags, separated flags, sudo, xargs/find piped)
@@ -69,7 +87,7 @@ DESTRUCTIVE_PATTERNS=(
69
87
  # Build combined regex
70
88
  REGEX=$(IFS='|'; echo "${DESTRUCTIVE_PATTERNS[*]}")
71
89
 
72
- if echo "$NORMALIZED" | grep -qEi "($REGEX)"; then
90
+ if echo "$SAFE_STRIPPED" | grep -qEi "($REGEX)"; then
73
91
  echo "WARNING: Potentially destructive command detected. Please verify." >&2
74
92
  exit 2
75
93
  fi
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # loop-guard.sh — Advisory detection of repeated identical actions (stuck loops).
3
+ #
4
+ # Fires on: PostToolUse (Bash|Edit|MultiEdit|Write)
5
+ # Never blocks. Emits an advisory when the SAME tool action repeats at least
6
+ # THRESHOLD times within a short window — catching successful-but-identical
7
+ # loops that the /repeat circuit breaker (which only counts FAILURES) misses.
8
+ # Skipped in minimal profile; honours AI_TOOLKIT_DISABLED_HOOKS.
9
+ #
10
+ # Tunables: AI_TOOLKIT_LOOP_WINDOW (default 6), AI_TOOLKIT_LOOP_THRESHOLD (3).
11
+
12
+ # shellcheck source=_profile-check.sh
13
+ source "$(dirname "$0")/_profile-check.sh"
14
+ # shellcheck source=_hook-io.sh
15
+ source "$(dirname "$0")/_hook-io.sh"
16
+
17
+ # shellcheck disable=SC2034 # INPUT is consumed via sourced _hook-io.sh
18
+ INPUT=$(cat)
19
+ TOOL_NAME=$(hook_tool_name)
20
+ [ -z "$TOOL_NAME" ] && exit 0
21
+
22
+ # Build an identity for the action. For Bash it is the command; for edits it is
23
+ # file + new content, so normal iterative editing of one file does NOT trip the
24
+ # guard — only re-applying an IDENTICAL change does.
25
+ CMD=$(hook_command)
26
+ if [ -n "$CMD" ]; then
27
+ IDENT="$CMD"
28
+ else
29
+ FILE=$(hook_file_path)
30
+ [ -z "$FILE" ] && exit 0
31
+ IDENT="${FILE}::$(hook_new_content)"
32
+ fi
33
+
34
+ SID=$(hook_session_id)
35
+ STATE_DIR="${HOME}/.softspark/ai-toolkit/sessions"
36
+ mkdir -p "$STATE_DIR" 2>/dev/null || exit 0
37
+ LOG="${STATE_DIR}/${SID}-actions.log"
38
+
39
+ WINDOW="${AI_TOOLKIT_LOOP_WINDOW:-6}"
40
+ THRESHOLD="${AI_TOOLKIT_LOOP_THRESHOLD:-3}"
41
+
42
+ # Store only a short hash of "tool|identity" — never the raw payload.
43
+ HASH=$(printf '%s|%s' "$TOOL_NAME" "$IDENT" | { shasum -a 256 2>/dev/null || sha256sum 2>/dev/null; } | cut -c1-16)
44
+ [ -z "$HASH" ] && exit 0
45
+ printf '%s\n' "$HASH" >> "$LOG" 2>/dev/null || exit 0
46
+
47
+ # Keep only the last WINDOW entries so the file stays tiny.
48
+ lines=$(wc -l < "$LOG" 2>/dev/null | tr -d ' ')
49
+ if [ -n "$lines" ] && [ "$lines" -gt "$WINDOW" ]; then
50
+ tail -n "$WINDOW" "$LOG" > "${LOG}.tmp" 2>/dev/null && mv "${LOG}.tmp" "$LOG" 2>/dev/null
51
+ fi
52
+
53
+ COUNT=$(grep -cxF "$HASH" "$LOG" 2>/dev/null | tr -d ' ')
54
+ [ -z "$COUNT" ] && COUNT=0
55
+ if [ "$COUNT" -ge "$THRESHOLD" ]; then
56
+ export AI_TOOLKIT_HOOK_FORMAT=json
57
+ hook_emit_context "PostToolUse" \
58
+ "Loop guard: the same ${TOOL_NAME} action has repeated ${COUNT}x within the last ${WINDOW} steps. If you are not making progress, stop and reassess — try a different approach or ask the user — instead of retrying the identical action."
59
+ fi
60
+ exit 0
@@ -58,9 +58,11 @@ if [ -f "$SESSION_FILE" ] && [ "${AI_TOOLKIT_HOOK_QUIET:-0}" != "1" ] && [ "${AI
58
58
  printf '%s\n' "====================="
59
59
  fi
60
60
 
61
- # 3. Load active instincts (if any)
61
+ # 3. Load active instincts (if any). These are explicit, hand-authored files, so
62
+ # they load by default whenever present (unlike auto-saved session context above,
63
+ # which stays verbose-gated). No instincts on disk => no output, no token cost.
62
64
  INSTINCTS_DIR=".claude/instincts"
63
- if [ -d "$INSTINCTS_DIR" ] && [ "${AI_TOOLKIT_HOOK_QUIET:-0}" != "1" ] && [ "${AI_TOOLKIT_HOOK_VERBOSE:-0}" = "1" ] && ls "$INSTINCTS_DIR"/*.md >/dev/null 2>&1; then
65
+ if [ -d "$INSTINCTS_DIR" ] && [ "${AI_TOOLKIT_HOOK_QUIET:-0}" != "1" ] && ls "$INSTINCTS_DIR"/*.md >/dev/null 2>&1; then
64
66
  printf '%s\n' "=== Active Instincts ==="
65
67
  for f in "$INSTINCTS_DIR"/*.md; do
66
68
  printf '%s\n' "- $(head -1 "$f")"
package/app/hooks.json CHANGED
@@ -139,6 +139,16 @@
139
139
  }
140
140
  ]
141
141
  },
142
+ {
143
+ "_source": "ai-toolkit",
144
+ "matcher": "Bash|Edit|MultiEdit|Write",
145
+ "hooks": [
146
+ {
147
+ "type": "command",
148
+ "command": "\"$HOME/.softspark/ai-toolkit/hooks/loop-guard.sh\""
149
+ }
150
+ ]
151
+ },
142
152
  {
143
153
  "_source": "ai-toolkit",
144
154
  "matcher": "Edit|MultiEdit|Write",
@@ -5,13 +5,13 @@ Persistent session memory with SQLite storage and full-text search.
5
5
  ## Includes
6
6
  - Skills: `/mem-search` (FTS5 search across past sessions)
7
7
  - Hooks: `observation-capture.sh` (PostToolUse), `session-summary.sh` (Stop)
8
- - Scripts: `init_db.py` (database setup), `strip_private.py` (privacy filter)
8
+ - Scripts: `init_db.py` (database setup), `strip_private.py` (privacy filter + secret redaction)
9
9
 
10
10
  ## How It Works
11
11
  1. **Capture**: Every tool use is recorded to `~/.softspark/ai-toolkit/memory.db` (PostToolUse hook)
12
12
  2. **Summarize**: Session summary generated on each Stop hook (tools used, observation count, time range)
13
13
  3. **Search**: `/mem-search <query>` runs FTS5 full-text search with progressive disclosure
14
- 4. **Privacy**: Content between `<private>...</private>` tags is stripped before storage
14
+ 4. **Privacy**: `strip_private.py` runs before storage — it removes `<private>...</private>` blocks and redacts high-confidence secrets (API keys, tokens, private keys) to `[REDACTED:<label>]`
15
15
 
16
16
  ## Database Schema
17
17
  - `sessions` — one row per Claude Code session (id, project_dir, started_at, ended_at, summary)
@@ -1,20 +1,47 @@
1
1
  #!/usr/bin/env python3
2
- """Remove <private>...</private> blocks from stdin.
2
+ """Sanitize stdin before it is stored in the session memory database.
3
3
 
4
- Used by observation-capture.sh to sanitize content before storage.
5
- Handles multi-line private blocks and nested angle brackets.
4
+ Two passes:
5
+ 1. Remove ``<private>...</private>`` blocks (explicit user opt-out).
6
+ 2. Redact high-confidence secrets (API keys, tokens, private keys) so a token
7
+ that scrolls through a tool output never lands in the SQLite memory store.
6
8
 
7
- Usage: echo "text <private>secret</private> more" | python3 strip_private.py
9
+ Used by observation-capture.sh and session-summary.sh.
10
+
11
+ Usage: echo "text <private>secret</private> sk-..." | python3 strip_private.py
8
12
  """
9
13
  from __future__ import annotations
10
14
 
11
15
  import re
12
16
  import sys
13
17
 
18
+ # High-confidence, structural secret patterns only. These match a specific
19
+ # token shape (not "password = ..." prose) so false positives stay near zero —
20
+ # important because over-redaction silently corrupts stored observations.
21
+ _SECRET_PATTERNS: list[tuple[re.Pattern[str], str]] = [
22
+ (re.compile(r"AKIA[0-9A-Z]{16}"), "aws-key"),
23
+ (re.compile(r"\bsk-[A-Za-z0-9]{20,}"), "api-key"),
24
+ (re.compile(r"\bgh[posru]_[A-Za-z0-9]{36,}"), "github-token"),
25
+ (re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{10,}"), "slack-token"),
26
+ (re.compile(r"\bAIza[0-9A-Za-z_\-]{35}"), "google-key"),
27
+ (re.compile(
28
+ r"-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"
29
+ r"[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"
30
+ ), "private-key"),
31
+ ]
32
+
33
+
34
+ def scrub_secrets(text: str) -> str:
35
+ """Replace recognised secret tokens with a labelled redaction marker."""
36
+ for pattern, label in _SECRET_PATTERNS:
37
+ text = pattern.sub(f"[REDACTED:{label}]", text)
38
+ return text
39
+
14
40
 
15
41
  def main() -> None:
16
42
  content = sys.stdin.read()
17
43
  cleaned = re.sub(r"<private>.*?</private>", "", content, flags=re.DOTALL)
44
+ cleaned = scrub_secrets(cleaned)
18
45
  sys.stdout.write(cleaned)
19
46
 
20
47
 
@@ -13,9 +13,9 @@ $ARGUMENTS
13
13
 
14
14
  ## What This Does
15
15
 
16
- Manages the instinct system that learns patterns from your sessions.
16
+ Manages the instinct system: project-local behavioral notes that Claude loads at the start of a session.
17
17
 
18
- Instincts are stored in `.claude/instincts/` and loaded at session start.
18
+ Instincts are markdown files in `.claude/instincts/`, loaded at session start by `session-start.sh` whenever any exist (set `AI_TOOLKIT_HOOK_QUIET=1` to suppress).
19
19
 
20
20
  ## Commands
21
21
 
@@ -48,10 +48,10 @@ Interactive review: shows each instinct and asks promote/remove/keep.
48
48
 
49
49
  ## How Instincts Work
50
50
 
51
- 1. **Extraction**: At session end, the Stop hook extracts patterns from the session
52
- 2. **Storage**: Saved as `.claude/instincts/<pattern-name>.md` with confidence score
53
- 3. **Loading**: At session start, instincts are loaded and shown to Claude
54
- 4. **Curation**: Use `/instinct-review` to promote good ones and remove bad ones
51
+ 1. **Authoring**: Instinct files are written by hand one markdown file per pattern in `.claude/instincts/`. There is no automatic session-end extractor today (see [When NOT to Use](#when-not-to-use)).
52
+ 2. **Storage**: `.claude/instincts/<pattern-name>.md`, each carrying a confidence score (see format below).
53
+ 3. **Loading**: At session start, `session-start.sh` loads every instinct file into context whenever any exist. No instincts on disk means nothing is loaded; set `AI_TOOLKIT_HOOK_QUIET=1` to suppress.
54
+ 4. **Curation**: Use `/instinct-review` to list, promote, remove, or clear them.
55
55
 
56
56
  ## Instinct Format
57
57
 
@@ -88,14 +88,14 @@ Last seen: 2026-03-25
88
88
  ## Gotchas
89
89
 
90
90
  - Instincts live in **project-local** `.claude/instincts/`, not in `~/.softspark/ai-toolkit/`. Running this skill in a different project sees a different set — do not treat the list as global state.
91
- - The Stop hook that extracts instincts may write draft files even when no meaningful pattern was observed. A `.claude/instincts/` with 40 files is usually 35 noise + 5 signal curate ruthlessly.
91
+ - Every instinct file loads into session context by default (whenever the directory is non-empty), so each one costs startup tokens. This is why curation matters prune aggressively and keep the set small.
92
92
  - Promoted instincts (confidence = 1.0) load into every session's context, costing tokens. Too many pinned instincts bloat startup. Keep ≤10 pinned.
93
- - Instinct files are plain markdown but the extraction format may evolve between ai-toolkit versions. A `--clear` after a version bump is often cleaner than trying to migrate old formats.
93
+ - Instinct files are plain markdown following the format above. If you accumulate many, a `--clear` and re-author is often cleaner than migrating stale notes after a major toolkit bump.
94
94
  - The filename (`<pattern-name>.md`) is the identity used by `--promote` and `--remove`. Renaming files manually breaks those flags until the user reopens the review UI.
95
95
 
96
96
  ## When NOT to Use
97
97
 
98
- - To **extract** new instincts — extraction is automatic via the Stop hook
98
+ - To **auto-extract** instincts from a session not implemented. No Stop hook writes instinct files; author them by hand (or via a future extraction tool). This skill curates existing files, it does not generate them.
99
99
  - To search past session memory — use `/mem-search`
100
100
  - To edit global toolkit memory files — those live in `~/.claude/projects/*/memory/` and are managed by the auto-memory system, not this skill
101
- - To permanently disable the instinct system edit `settings.json` to remove the Stop hook; `--clear` only wipes current state, extraction continues on the next session
101
+ - To stop instincts from loading — delete the files with `--clear`, or set `AI_TOOLKIT_HOOK_QUIET=1` to suppress all session-start output. There is no extraction process to disable.
@@ -73,6 +73,16 @@ Scan skill and agent definitions for security risks before installation or after
73
73
  | `-----BEGIN.*PRIVATE KEY` | HIGH | Private key |
74
74
  | `ghp_[a-zA-Z0-9]{36}` | HIGH | GitHub PAT |
75
75
 
76
+ ### Unicode Safety
77
+
78
+ Shipped prompt text (skills, agents, rules, personas, mcp-templates) is scanned for invisible/smuggled characters that a human reviewer cannot see. `U+200D` (ZWJ) is allowlisted so legitimate emoji sequences do not flag.
79
+
80
+ | Pattern | Severity | Description |
81
+ |---------|----------|-------------|
82
+ | Tag block `U+E0000–U+E007F` | HIGH | ASCII smuggling / invisible prompt injection |
83
+ | Bidi controls (LRE/RLE/PDF/LRO/RLO, LRI/RLI/FSI/PDI) | HIGH | Trojan Source text reordering |
84
+ | Zero-width / invisible format chars (ZWSP, ZWNJ, WJ, BOM, soft hyphen, …) | WARN | Verify it is intentional |
85
+
76
86
  ## Output Format
77
87
 
78
88
  ```markdown
@@ -105,6 +105,25 @@ CORRECT: Agent reports success → Check VCS diff → Verify changes → Report
105
105
  WRONG: Trust agent report at face value
106
106
  ```
107
107
 
108
+ ## Live-App Rubric Verification (optional)
109
+
110
+ When the success criterion is **behavioral or visual** (a UI flow, a generated app, a multi-step interaction), a pass/fail command is not enough — the proof is the running app, observed. Use a weighted rubric instead of a single assertion:
111
+
112
+ 1. **Define the rubric BEFORE building** — 3–6 criteria, each with a weight and an explicit pass bar. Example:
113
+
114
+ | Criterion | Weight | Pass bar |
115
+ |-----------|--------|----------|
116
+ | Core flow completes end-to-end | 0.40 | No error, reaches success state |
117
+ | Empty / loading / error states render | 0.25 | All three visible |
118
+ | Matches the requested layout | 0.20 | No major deviation |
119
+ | No console errors | 0.15 | Console clean |
120
+
121
+ 2. **Launch the app and observe** — actually run it and capture the behavior (screenshot, console, network). Do not infer from the source.
122
+ 3. **Score with a fresh evaluator** — have an independent agent grade the observed behavior against the rubric, not the implementer who wrote it (self-grading anchors high). Compute the weighted score.
123
+ 4. **Gate on the threshold** — below the bar (e.g. < 0.8) the claim is NOT verified: list the failing criteria as concrete defects and iterate. At or above the bar, state the score WITH the captured evidence.
124
+
125
+ The rubric is the verification command for work that has no green/red exit code. The same Iron Law applies: observed evidence before the claim, every time.
126
+
108
127
  ## Constitutional Anchors
109
128
 
110
129
  This skill enforces **Constitution Art. VI.4 (Verify Before Claiming Done)**. The diff re-read is not optional: before any completion claim, confirm no orphaned references, no missing test coverage for changed paths, no stale docs. A task is not done while any of those exist.
@@ -3,7 +3,7 @@ title: "Hooks Catalog"
3
3
  category: reference
4
4
  service: ai-toolkit
5
5
  tags: [hooks, quality, safety, enforcement, settings.json]
6
- version: "1.5.6"
6
+ version: "1.5.7"
7
7
  created: "2026-03-27"
8
8
  last_updated: "2026-05-25"
9
9
  description: "Complete reference of all ai-toolkit hooks: events, scripts, installation, and runtime behavior."
@@ -13,7 +13,7 @@ description: "Complete reference of all ai-toolkit hooks: events, scripts, insta
13
13
 
14
14
  ## Overview
15
15
 
16
- ai-toolkit provides 28 global hook entries across 14 lifecycle events that enforce quality, safety, and workflow rules across all Claude Code sessions. Hooks are merged into `~/.claude/settings.json` on install, with logic in standalone scripts at `~/.softspark/ai-toolkit/hooks/`.
16
+ ai-toolkit provides 29 global hook entries across 14 lifecycle events that enforce quality, safety, and workflow rules across all Claude Code sessions. Hooks are merged into `~/.claude/settings.json` on install, with logic in standalone scripts at `~/.softspark/ai-toolkit/hooks/`.
17
17
 
18
18
  ## Supported Surface
19
19
 
@@ -85,6 +85,10 @@ for debugging; `AI_TOOLKIT_HOOK_QUIET=1` keeps it silent explicitly.
85
85
  - `git push --force`
86
86
  - `chmod -R 777`
87
87
 
88
+ **Exemptions (avoid false positives):**
89
+ - `git push --force-with-lease` / `--force-if-includes` — the safe force-push variants are allowed.
90
+ - A single, non-chained `echo`/`printf`/`git commit`/`git tag` carrying a destructive token as *data* (e.g. a commit message mentioning `DROP TABLE`) is allowed. Chained commands (`&&`, `;`, `|`) are still inspected in full.
91
+
88
92
  ### PreToolUse (file ops) — `guard-path.sh`
89
93
 
90
94
  | Field | Value |
@@ -403,6 +407,19 @@ First-match-wins per file. Built-in runners: `bats`, `pytest`, `vitest`, `jest`.
403
407
 
404
408
  **Overrides:** `CLAUDE_SKIP_COHESION=1` (one-off), `CLAUDE_HOOK_BOOTSTRAP=1` (when editing the hook itself). Skipped when `TOOLKIT_HOOK_PROFILE=minimal`.
405
409
 
410
+ ### PostToolUse (loop guard) — `loop-guard.sh`
411
+
412
+ | Field | Value |
413
+ |-------|-------|
414
+ | Event | `PostToolUse` |
415
+ | Matcher | `Bash\|Edit\|MultiEdit\|Write` |
416
+ | Script | `~/.softspark/ai-toolkit/hooks/loop-guard.sh` |
417
+ | Fires | After every Bash command or file edit |
418
+
419
+ **Action:** Advisory only, never blocks. Hashes a `tool|identity` signature of each action (command for Bash; file path + new content for edits) and keeps the last `AI_TOOLKIT_LOOP_WINDOW` (default 6) hashes per session. When the same signature repeats `AI_TOOLKIT_LOOP_THRESHOLD` times (default 3) it emits a `PostToolUse` `additionalContext` advisory telling Claude to reassess instead of retrying. Catches successful-but-identical loops that the `/repeat` circuit breaker (which only counts failures) does not. Edits track content, so normal iterative editing of one file does not trip it. Only short hashes are stored — never raw payloads.
420
+
421
+ **Tunables:** `AI_TOOLKIT_LOOP_WINDOW`, `AI_TOOLKIT_LOOP_THRESHOLD`. Skipped when `TOOLKIT_HOOK_PROFILE=minimal`; honours `AI_TOOLKIT_DISABLED_HOOKS`.
422
+
406
423
  ### PostToolUse (search-first tracker) — `search-tracker.sh`
407
424
 
408
425
  | Field | Value |
@@ -480,6 +497,8 @@ Set in `.claude/settings.local.json`:
480
497
  | `standard` | All hooks (default) |
481
498
  | `strict` | Standard + mypy --strict on task completion |
482
499
 
500
+ **Disabling individual hooks:** set `AI_TOOLKIT_DISABLED_HOOKS` to a comma-separated list of hook names (with or without `.sh`), e.g. `{ "env": { "AI_TOOLKIT_DISABLED_HOOKS": "loop-guard,quality-check" } }`. Listed hooks become no-ops. This covers profile-gated hooks only; the safety guards (`guard-destructive`/`guard-path`/`guard-config`) intentionally cannot be env-disabled — remove them deliberately with `ai-toolkit remove-hook`.
501
+
483
502
  Non-blocking informational context is silent in plain-text mode by default while
484
503
  side effects and blocking decisions still run. Set `AI_TOOLKIT_HOOK_VERBOSE=1`
485
504
  only when debugging hook context locally. `AI_TOOLKIT_HOOK_QUIET=1` keeps hook
@@ -561,7 +580,7 @@ commands explicitly silent, and Codex-generated hooks plus Claude's bundled
561
580
 
562
581
  **Hook script not found:**
563
582
  ```bash
564
- ls ~/.softspark/ai-toolkit/hooks/ # should list 27 .sh files (plus _profile-check.sh + _locate-toolkit.sh + _hook-io.sh + _search-capability.sh helpers + test-cohesion-map.json)
583
+ ls ~/.softspark/ai-toolkit/hooks/ # should list 28 .sh files (plus _profile-check.sh + _locate-toolkit.sh + _hook-io.sh + _search-capability.sh helpers + test-cohesion-map.json)
565
584
  ai-toolkit update # re-copies scripts
566
585
  ```
567
586
 
package/llms-full.txt CHANGED
@@ -8895,7 +8895,7 @@ title: "Hooks Catalog"
8895
8895
  category: reference
8896
8896
  service: ai-toolkit
8897
8897
  tags: [hooks, quality, safety, enforcement, settings.json]
8898
- version: "1.5.6"
8898
+ version: "1.5.7"
8899
8899
  created: "2026-03-27"
8900
8900
  last_updated: "2026-05-25"
8901
8901
  description: "Complete reference of all ai-toolkit hooks: events, scripts, installation, and runtime behavior."
@@ -8905,7 +8905,7 @@ description: "Complete reference of all ai-toolkit hooks: events, scripts, insta
8905
8905
 
8906
8906
  ## Overview
8907
8907
 
8908
- ai-toolkit provides 28 global hook entries across 14 lifecycle events that enforce quality, safety, and workflow rules across all Claude Code sessions. Hooks are merged into `~/.claude/settings.json` on install, with logic in standalone scripts at `~/.softspark/ai-toolkit/hooks/`.
8908
+ ai-toolkit provides 29 global hook entries across 14 lifecycle events that enforce quality, safety, and workflow rules across all Claude Code sessions. Hooks are merged into `~/.claude/settings.json` on install, with logic in standalone scripts at `~/.softspark/ai-toolkit/hooks/`.
8909
8909
 
8910
8910
  ## Supported Surface
8911
8911
 
@@ -8977,6 +8977,10 @@ for debugging; `AI_TOOLKIT_HOOK_QUIET=1` keeps it silent explicitly.
8977
8977
  - `git push --force`
8978
8978
  - `chmod -R 777`
8979
8979
 
8980
+ **Exemptions (avoid false positives):**
8981
+ - `git push --force-with-lease` / `--force-if-includes` — the safe force-push variants are allowed.
8982
+ - A single, non-chained `echo`/`printf`/`git commit`/`git tag` carrying a destructive token as *data* (e.g. a commit message mentioning `DROP TABLE`) is allowed. Chained commands (`&&`, `;`, `|`) are still inspected in full.
8983
+
8980
8984
  ### PreToolUse (file ops) — `guard-path.sh`
8981
8985
 
8982
8986
  | Field | Value |
@@ -9295,6 +9299,19 @@ First-match-wins per file. Built-in runners: `bats`, `pytest`, `vitest`, `jest`.
9295
9299
 
9296
9300
  **Overrides:** `CLAUDE_SKIP_COHESION=1` (one-off), `CLAUDE_HOOK_BOOTSTRAP=1` (when editing the hook itself). Skipped when `TOOLKIT_HOOK_PROFILE=minimal`.
9297
9301
 
9302
+ ### PostToolUse (loop guard) — `loop-guard.sh`
9303
+
9304
+ | Field | Value |
9305
+ |-------|-------|
9306
+ | Event | `PostToolUse` |
9307
+ | Matcher | `Bash\|Edit\|MultiEdit\|Write` |
9308
+ | Script | `~/.softspark/ai-toolkit/hooks/loop-guard.sh` |
9309
+ | Fires | After every Bash command or file edit |
9310
+
9311
+ **Action:** Advisory only, never blocks. Hashes a `tool|identity` signature of each action (command for Bash; file path + new content for edits) and keeps the last `AI_TOOLKIT_LOOP_WINDOW` (default 6) hashes per session. When the same signature repeats `AI_TOOLKIT_LOOP_THRESHOLD` times (default 3) it emits a `PostToolUse` `additionalContext` advisory telling Claude to reassess instead of retrying. Catches successful-but-identical loops that the `/repeat` circuit breaker (which only counts failures) does not. Edits track content, so normal iterative editing of one file does not trip it. Only short hashes are stored — never raw payloads.
9312
+
9313
+ **Tunables:** `AI_TOOLKIT_LOOP_WINDOW`, `AI_TOOLKIT_LOOP_THRESHOLD`. Skipped when `TOOLKIT_HOOK_PROFILE=minimal`; honours `AI_TOOLKIT_DISABLED_HOOKS`.
9314
+
9298
9315
  ### PostToolUse (search-first tracker) — `search-tracker.sh`
9299
9316
 
9300
9317
  | Field | Value |
@@ -9372,6 +9389,8 @@ Set in `.claude/settings.local.json`:
9372
9389
  | `standard` | All hooks (default) |
9373
9390
  | `strict` | Standard + mypy --strict on task completion |
9374
9391
 
9392
+ **Disabling individual hooks:** set `AI_TOOLKIT_DISABLED_HOOKS` to a comma-separated list of hook names (with or without `.sh`), e.g. `{ "env": { "AI_TOOLKIT_DISABLED_HOOKS": "loop-guard,quality-check" } }`. Listed hooks become no-ops. This covers profile-gated hooks only; the safety guards (`guard-destructive`/`guard-path`/`guard-config`) intentionally cannot be env-disabled — remove them deliberately with `ai-toolkit remove-hook`.
9393
+
9375
9394
  Non-blocking informational context is silent in plain-text mode by default while
9376
9395
  side effects and blocking decisions still run. Set `AI_TOOLKIT_HOOK_VERBOSE=1`
9377
9396
  only when debugging hook context locally. `AI_TOOLKIT_HOOK_QUIET=1` keeps hook
@@ -9453,7 +9472,7 @@ commands explicitly silent, and Codex-generated hooks plus Claude's bundled
9453
9472
 
9454
9473
  **Hook script not found:**
9455
9474
  ```bash
9456
- ls ~/.softspark/ai-toolkit/hooks/ # should list 27 .sh files (plus _profile-check.sh + _locate-toolkit.sh + _hook-io.sh + _search-capability.sh helpers + test-cohesion-map.json)
9475
+ ls ~/.softspark/ai-toolkit/hooks/ # should list 28 .sh files (plus _profile-check.sh + _locate-toolkit.sh + _hook-io.sh + _search-capability.sh helpers + test-cohesion-map.json)
9457
9476
  ai-toolkit update # re-copies scripts
9458
9477
  ```
9459
9478
 
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.4.2",
2
+ "version": "4.5.1",
3
3
  "components": {
4
4
  "agents": {
5
5
  "description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "4.4.2",
3
+ "version": "4.5.1",
4
4
  "description": "AI coding toolkit: 107 skills, 44 agents, 12-editor write-through (Claude, Cursor, Windsurf, Copilot, Gemini, Cline, Roo, Aider, Augment, Antigravity, Codex, opencode), machine-enforced safety constitution, SARIF audit, signed npm provenance.",
5
5
  "keywords": [
6
6
  "claude",
@@ -2,7 +2,8 @@
2
2
  """Skill & Agent Security Auditor.
3
3
 
4
4
  Deterministic scanner for ai-toolkit skills and agents.
5
- Detects dangerous code patterns, hardcoded secrets, and permission issues.
5
+ Detects dangerous code patterns, hardcoded secrets, permission issues, and
6
+ invisible/smuggled Unicode in shipped prompt text.
6
7
 
7
8
  Stdlib-only. JSON output to stdout. Non-zero exit on HIGH findings.
8
9
 
@@ -22,6 +23,7 @@ from __future__ import annotations
22
23
  import json
23
24
  import re
24
25
  import sys
26
+ import unicodedata
25
27
  from pathlib import Path
26
28
 
27
29
  sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -149,6 +151,73 @@ def scan_secrets(filepath: Path, findings: list[Finding]) -> None:
149
151
  findings.append(Finding(severity, rel, lineno, regex, desc))
150
152
 
151
153
 
154
+ # ---------------------------------------------------------------------------
155
+ # Unicode safety — invisible / smuggled characters in shipped prompt text.
156
+ # Prompt files (skills, agents, rules, personas, mcp-templates) ARE the
157
+ # product. A poisoned PR can carry instructions a human reviewer cannot see:
158
+ # regex-over-decoded-text checks (above) never catch them.
159
+ # ---------------------------------------------------------------------------
160
+
161
+ # ASCII smuggling: U+E0000–U+E007F mirror ASCII inside Unicode "tag" chars.
162
+ _UNICODE_TAG_BLOCK = range(0xE0000, 0xE0080)
163
+
164
+ # Trojan Source bidi controls (CVE-2021-42574) — reorder rendered text vs. its
165
+ # logical/source order, hiding instructions from a reviewer.
166
+ _UNICODE_BIDI_HIGH = frozenset({
167
+ 0x202A, 0x202B, 0x202C, 0x202D, 0x202E, # LRE RLE PDF LRO RLO
168
+ 0x2066, 0x2067, 0x2068, 0x2069, # LRI RLI FSI PDI
169
+ })
170
+
171
+ # Invisible / zero-width formatting that can hide or splice text.
172
+ # U+200D (ZERO WIDTH JOINER) is deliberately EXCLUDED — it is required by
173
+ # legitimate emoji sequences and i18n text, so flagging it floods false
174
+ # positives and trains maintainers to disable the gate.
175
+ _UNICODE_INVISIBLE_WARN = frozenset({
176
+ 0x200B, 0x200C, 0x200E, 0x200F, 0x2060, 0xFEFF, 0x00AD,
177
+ 0x180E, 0x115F, 0x1160, 0x3164, 0xFFA0, 0x17B4, 0x17B5,
178
+ })
179
+
180
+
181
+ def _char_name(cp: int) -> str:
182
+ """Best-effort Unicode name for diagnostics."""
183
+ try:
184
+ return unicodedata.name(chr(cp))
185
+ except ValueError:
186
+ return f"U+{cp:04X}"
187
+
188
+
189
+ def scan_unicode(filepath: Path, findings: list[Finding]) -> None:
190
+ """Flag invisible / smuggled Unicode in shipped prompt text."""
191
+ try:
192
+ text = filepath.read_text(encoding="utf-8", errors="replace")
193
+ except OSError:
194
+ return
195
+ rel = str(filepath)
196
+ for lineno, line in enumerate(text.splitlines(), 1):
197
+ for ch in line:
198
+ cp = ord(ch)
199
+ if cp < 0x80: # plain ASCII — fast path, always safe
200
+ continue
201
+ if cp in _UNICODE_TAG_BLOCK:
202
+ findings.append(Finding(
203
+ "HIGH", rel, lineno, "unicode-tag-block",
204
+ f"Unicode tag char {_char_name(cp)} (U+{cp:04X}) — "
205
+ "ASCII smuggling / invisible prompt injection",
206
+ ))
207
+ elif cp in _UNICODE_BIDI_HIGH:
208
+ findings.append(Finding(
209
+ "HIGH", rel, lineno, "unicode-bidi-control",
210
+ f"Bidi control {_char_name(cp)} (U+{cp:04X}) — "
211
+ "Trojan Source text reordering",
212
+ ))
213
+ elif cp in _UNICODE_INVISIBLE_WARN:
214
+ findings.append(Finding(
215
+ "WARN", rel, lineno, "unicode-invisible",
216
+ f"Invisible/zero-width char {_char_name(cp)} (U+{cp:04X}) — "
217
+ "verify it is intentional",
218
+ ))
219
+
220
+
152
221
  # Description quality — per Anthropic docs (code.claude.com/docs/en/skills.md):
153
222
  # description + when_to_use combined ≤ 1536 chars; first sentence carries
154
223
  # the trigger keywords that let the LLM route to this skill.
@@ -373,15 +442,26 @@ def audit(toolkit_root: Path) -> list[Finding]:
373
442
  scan_file_patterns(sh, BASH_WARN, "WARN", findings)
374
443
  scan_secrets(sh, findings)
375
444
 
376
- # Secrets in any text file
445
+ # Secrets + invisible Unicode in any text file
377
446
  for md in skill_dir.rglob("*.md"):
378
447
  scan_secrets(md, findings)
448
+ scan_unicode(md, findings)
379
449
 
380
450
  # Scan agents
381
451
  if agents.is_dir():
382
452
  for agent_md in sorted(agents.glob("*.md")):
383
453
  check_agent(agent_md, findings)
384
454
  scan_secrets(agent_md, findings)
455
+ scan_unicode(agent_md, findings)
456
+
457
+ # Unicode safety across the rest of the shipped prompt surface.
458
+ for extra in ("rules", "personas", "mcp-templates"):
459
+ extra_dir = app / extra
460
+ if not extra_dir.is_dir():
461
+ continue
462
+ for f in sorted(extra_dir.rglob("*")):
463
+ if f.is_file() and f.suffix in (".md", ".json"):
464
+ scan_unicode(f, findings)
385
465
 
386
466
  # Sort: HIGH first, then WARN, then INFO
387
467
  order = {"HIGH": 0, "WARN": 1, "INFO": 2}
package/scripts/doctor.py CHANGED
@@ -60,6 +60,7 @@ EXPECTED_HOOKS = [
60
60
  "config-desync-guard.sh",
61
61
  "instructions-audit.sh",
62
62
  "post-tool-use.sh",
63
+ "loop-guard.sh",
63
64
  "quality-check.sh",
64
65
  "quality-gate.sh",
65
66
  "revert-guard.sh",
@@ -549,6 +550,55 @@ def check_url_hooks(dr: DiagResult, fix_mode: bool) -> None:
549
550
  print(f" Could not re-fetch: {exc}")
550
551
 
551
552
 
553
+ # ---------------------------------------------------------------------------
554
+ # Check 10: Language Rules Drift (project-local)
555
+ # ---------------------------------------------------------------------------
556
+
557
+ def check_language_drift(dr: DiagResult) -> None:
558
+ """Warn when the current project gained a language but its rules were not injected.
559
+
560
+ Runs only inside a local-install project (cwd has .claude/CLAUDE.md with the
561
+ language-rules block). A new Cargo.toml / go.mod that appears after install
562
+ otherwise leaves the matching <lang>-rules unlinked with zero signal.
563
+ """
564
+ print()
565
+ print("## 10. Language Rules Drift")
566
+
567
+ claude_md = Path.cwd() / ".claude" / "CLAUDE.md"
568
+ if not claude_md.is_file():
569
+ dr.skip("not a local-install project (no .claude/CLAUDE.md)")
570
+ return
571
+ try:
572
+ content = claude_md.read_text(encoding="utf-8")
573
+ except OSError:
574
+ dr.skip("could not read .claude/CLAUDE.md")
575
+ return
576
+ if "TOOLKIT:language-rules" not in content:
577
+ dr.skip("no language-rules block in .claude/CLAUDE.md")
578
+ return
579
+
580
+ try:
581
+ from install_steps.detect_language import detect_languages
582
+ except Exception:
583
+ dr.skip("language detection unavailable")
584
+ return
585
+
586
+ missing = []
587
+ for module in detect_languages(Path.cwd(), toolkit_dir):
588
+ if not module.startswith("rules-"):
589
+ continue
590
+ skill = f"{module[len('rules-'):]}-rules"
591
+ if skill not in content:
592
+ missing.append(skill)
593
+
594
+ if not missing:
595
+ dr.ok("project language rules in sync")
596
+ return
597
+ for skill in missing:
598
+ lang = skill[: -len("-rules")]
599
+ dr.warn(f"{lang} detected but {skill} not injected — run: ai-toolkit install --local --lang {lang}")
600
+
601
+
552
602
  # ---------------------------------------------------------------------------
553
603
  # Main
554
604
  # ---------------------------------------------------------------------------
@@ -571,6 +621,7 @@ def main() -> None:
571
621
  check_benchmark_freshness(dr)
572
622
  check_stale_rules(dr, fix_mode)
573
623
  check_url_hooks(dr, fix_mode)
624
+ check_language_drift(dr)
574
625
 
575
626
  # Summary
576
627
  print("========================")
@@ -785,6 +785,9 @@ def validate_metadata_contracts(
785
785
  # Cross-validate versions: package.json vs manifest.json vs plugin.json
786
786
  _validate_version_sync(tk_dir, vr)
787
787
 
788
+ # Cross-validate the README platform matrix's Hooks column against reality
789
+ _validate_editor_hooks_honesty(tk_dir, vr)
790
+
788
791
  print()
789
792
  return actual_tests
790
793
 
@@ -836,6 +839,87 @@ def _validate_version_sync(tk_dir: Path, vr: ValidationResult) -> None:
836
839
  )
837
840
 
838
841
 
842
+ # Editors that receive lifecycle hook enforcement without a generate_*_hooks.py
843
+ # generator: Claude is native (app/hooks), opencode bridges via its plugin
844
+ # (generate_opencode_plugin.py). Every other hook-enabled editor is derived
845
+ # from the generate_<editor>_hooks.py set so this check stays honest as
846
+ # generators come and go.
847
+ _NATIVE_HOOK_EDITORS = {"claude", "opencode"}
848
+
849
+ # README platform label (lowercased) -> canonical editor key.
850
+ _README_PLATFORM_KEY = {
851
+ "claude code": "claude",
852
+ "cursor": "cursor",
853
+ "windsurf": "windsurf",
854
+ "gemini cli": "gemini",
855
+ "github copilot": "copilot",
856
+ "cline": "cline",
857
+ "roo code": "roo",
858
+ "aider": "aider",
859
+ "augment": "augment",
860
+ "google antigravity": "antigravity",
861
+ "codex cli": "codex",
862
+ "opencode": "opencode",
863
+ }
864
+
865
+
866
+ def _validate_editor_hooks_honesty(tk_dir: Path, vr: ValidationResult) -> None:
867
+ """Ensure the README platform matrix's Hooks column matches reality.
868
+
869
+ Guards against the overclaim that every editor gets hook enforcement when
870
+ only a subset does — the machine-enforced constitution needs generated
871
+ hooks, and rules-only editors receive guidance text without blocking hooks.
872
+ """
873
+ scripts_dir = tk_dir / "scripts"
874
+ readme = tk_dir / "README.md"
875
+ if not scripts_dir.is_dir() or not readme.is_file():
876
+ return # installed copy without source — nothing to cross-check
877
+
878
+ # Actual hook-enabled editors: native set + generate_<editor>_hooks.py stems.
879
+ actual = set(_NATIVE_HOOK_EDITORS)
880
+ for gen in scripts_dir.glob("generate_*_hooks.py"):
881
+ stem = gen.name[len("generate_"):-len("_hooks.py")]
882
+ actual.add(stem)
883
+
884
+ content = readme.read_text(encoding="utf-8")
885
+ if "| Hooks |" not in content:
886
+ return # matrix has no Hooks column to validate
887
+
888
+ claimed: set[str] = set()
889
+ parsed_any = False
890
+ for line in content.splitlines():
891
+ if not line.startswith("|"):
892
+ continue
893
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
894
+ if len(cells) < 4:
895
+ continue
896
+ key = _README_PLATFORM_KEY.get(cells[0].lower())
897
+ if key is None:
898
+ continue # header, separator, or unknown row
899
+ parsed_any = True
900
+ if "✅" in cells[2]: # ✅
901
+ claimed.add(key)
902
+
903
+ if not parsed_any:
904
+ return
905
+
906
+ missing = actual - claimed # has hooks but README omits the ✅
907
+ overclaimed = claimed - actual # README claims ✅ but no generator exists
908
+ if missing:
909
+ vr.error(
910
+ "README Hooks column understates enforcement for: "
911
+ + ", ".join(sorted(missing))
912
+ )
913
+ if overclaimed:
914
+ vr.error(
915
+ "README Hooks column overclaims enforcement for: "
916
+ + ", ".join(sorted(overclaimed))
917
+ + " (no hook generator or native bridge found)"
918
+ )
919
+ if not missing and not overclaimed:
920
+ print(f" OK: editor hooks honesty ({len(claimed)} hook-enabled editors)")
921
+
922
+
839
923
  ROMAN_NUMERALS = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"]
840
924
 
841
925