@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 +33 -0
- package/README.md +28 -27
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/ARCHITECTURE.md +1 -1
- package/app/hooks/_profile-check.sh +17 -0
- package/app/hooks/guard-destructive.sh +19 -1
- package/app/hooks/loop-guard.sh +60 -0
- package/app/hooks/session-start.sh +4 -2
- package/app/hooks.json +10 -0
- package/app/plugins/memory-pack/README.md +2 -2
- package/app/plugins/memory-pack/scripts/strip_private.py +31 -4
- package/app/skills/instinct-review/SKILL.md +10 -10
- package/app/skills/skill-audit/SKILL.md +10 -0
- package/app/skills/verification-before-completion/SKILL.md +19 -0
- package/kb/reference/hooks-catalog.md +22 -3
- package/llms-full.txt +22 -3
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/audit_skills.py +82 -2
- package/scripts/doctor.py +51 -0
- package/scripts/validate.py +84 -0
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)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
10
|
|
|
11
|
-
## What's New in v4.
|
|
11
|
+
## What's New in v4.5.1
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
-
|
|
18
|
-
- **
|
|
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`.
|
|
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/` |
|
|
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 (
|
|
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 (
|
|
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
|
-
**
|
|
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
|
+
"version": "4.5.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "SoftSpark",
|
|
7
7
|
"url": "https://github.com/softspark"
|
package/app/ARCHITECTURE.md
CHANGED
|
@@ -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 /
|
|
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 "$
|
|
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" ] &&
|
|
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**:
|
|
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
|
-
"""
|
|
2
|
+
"""Sanitize stdin before it is stored in the session memory database.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
16
|
+
Manages the instinct system: project-local behavioral notes that Claude loads at the start of a session.
|
|
17
17
|
|
|
18
|
-
Instincts are
|
|
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. **
|
|
52
|
-
2. **Storage**:
|
|
53
|
-
3. **Loading**: At session start, instincts
|
|
54
|
-
4. **Curation**: Use `/instinct-review` to promote
|
|
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
|
-
-
|
|
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
|
|
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**
|
|
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
|
|
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
|
+
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.
|
|
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",
|
package/scripts/audit_skills.py
CHANGED
|
@@ -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,
|
|
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("========================")
|
package/scripts/validate.py
CHANGED
|
@@ -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
|
|