Three fixes for the positive feedback loop causing runaway memory usage:
1. SIGUSR1 throttling in observe.sh: Signal observer only every 20
observations (configurable via ECC_OBSERVER_SIGNAL_EVERY_N) instead
of on every tool call. Uses a counter file to track invocations.
2. Re-entrancy guard in observer-loop.sh on_usr1(): ANALYZING flag
prevents parallel Claude analysis processes from spawning when
signals arrive while analysis is already running.
3. Cooldown + tail-based sampling in observer-loop.sh:
- 60s cooldown between analyses (ECC_OBSERVER_ANALYSIS_COOLDOWN)
- Only last 500 lines sent to LLM (ECC_OBSERVER_MAX_ANALYSIS_LINES)
instead of the entire observations file
Closes#521
* feat(continuous-learning-v2): add lazy-start observer logic
Auto-starts observer when observer.enabled: true in config and no .observer.pid exists.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(continuous-learning-v2): address PR review concerns
- Use flock for atomic check-then-act to prevent race conditions
- Check both project-scoped AND global PID files before starting
- Support CLV2_CONFIG override for config file path
- Check disabled file in lazy-start logic
- Use double-check pattern after acquiring lock
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(observe.sh): address PR review comments
- Add stale PID cleanup via _CHECK_OBSERVER_RUNNING function
- Add macOS fallback using lockfile when flock unavailable
- Fix CLV2_CONFIG override: use EFFECTIVE_CONFIG for both check and read
- Use proper Python context manager (with open() as f)
- Deduplicate signaled PIDs to avoid duplicate USR1 signals
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(observe.sh): wrap macOS lockfile fallback in subshell with trap
- Wrap lockfile block in subshell so exit 0 only terminates that block
- Add trap for EXIT to clean up lock file on script interruption
- Add -l 30 (30 second expiry) to prevent permanent lock file stuck
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(observe.sh): address remaining PR review comments
- Validate PID is a positive integer before kill calls to prevent
signaling invalid targets (e.g. -1 could signal all processes)
- Pass config path via env var instead of interpolating shell variable
into Python -c string to prevent injection/breakage on special paths
- Check CLV2_CONFIG-derived directory for disabled file so disable
guard respects the same config source as lazy-start
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Claude Code sends tool output as `tool_response` in PostToolUse hook
payloads, but observe.sh only checked for `tool_output` and `output`.
This caused all observations to have empty output fields, making the
observer pipeline blind to tool results.
Adds `tool_response` as the primary field to check, with backward-
compatible fallback to the existing `tool_output` and `output` fields.
* fix(hooks): scrub secrets and harden hook security
- Scrub common secret patterns (api_key, token, password, etc.) from
observation logs before persisting to JSONL (observe.sh)
- Auto-purge observation files older than 30 days (observe.sh)
- Strip embedded credentials from git remote URLs before saving to
projects.json (detect-project.sh)
- Add command prefix allowlist to runCommand — only git, node, npx,
which, where are permitted (utils.js)
- Sanitize CLAUDE_SESSION_ID in temp file paths to prevent path
traversal (suggest-compact.js)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(hooks): address review feedback from CodeRabbit and Cubic
- Reject shell command-chaining operators (;|&`) in runCommand, strip
quoted sections before checking to avoid false positives (utils.js)
- Remove command string from blocked error message to avoid leaking
secrets (utils.js)
- Fix Python regex quoting: switch outer shell string from double to
single quotes so regex compiles correctly (observe.sh)
- Add optional auth scheme match (Bearer, Basic) to secret scrubber
regex (observe.sh)
- Scope auto-purge to current project dir and match only archived
files (observations-*.jsonl), not live queue (observe.sh)
- Add second fallback after session ID sanitization to prevent empty
string (suggest-compact.js)
- Preserve backward compatibility when credential stripping changes
project hash — detect and migrate legacy directories
(detect-project.sh)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(hooks): block $() substitution, fix Bearer redaction, add security tests
- Add $ and \n to blocked shell metacharacters in runCommand to prevent
command substitution via $(cmd) and newline injection (utils.js)
- Make auth scheme group capturing so Bearer/Basic is preserved in
redacted output instead of being silently dropped (observe.sh)
- Add 10 unit tests covering runCommand allowlist blocking (rm, curl,
bash prefixes) and metacharacter rejection (;|&`$ chaining), plus
error message leak prevention (utils.test.js)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(hooks): scrub parse-error fallback, strengthen security tests
Address remaining reviewer feedback from CodeRabbit and Cubic:
- Scrub secrets in observe.sh parse-error fallback path (was writing
raw unsanitized input to observations file)
- Remove redundant re.IGNORECASE flag ((?i) inline flag already set)
- Add inline comment documenting quote-stripping limitation trade-off
- Fix misleading test name for error-output test
- Add 5 new security tests: single-quote passthrough, mixed
quoted+unquoted metacharacters, prefix boundary (no trailing space),
npx acceptance, and newline injection
- Improve existing quoted-metacharacter test to actually exercise
quote-stripping logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): block $() and backtick inside quotes in runCommand
Shell evaluates $() and backticks inside double quotes, so checking
only the unquoted portion was insufficient. Now $ and ` are rejected
anywhere in the command string, while ; | & remain quote-aware.
Addresses CodeRabbit and Cubic review feedback on PR #348.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Changes based on CodeRabbit review feedback:
1. Remove entire .cursor/ directory — it was an identical copy of the
main skills/commands/agents/rules, causing maintenance drift.
Users of Cursor can reference the canonical files directly.
2. Use explicit `is not None` checks instead of truthiness for
parsed['input'] and parsed['output']. Empty strings or empty
dicts are valid values that should be preserved.
The observe.sh script receives "pre" or "post" as $1 from the hook
config, but the Python code was looking for a "hook_type" field in
the stdin JSON. Claude Code does NOT include "hook_type" in the
JSON payload passed to hooks, so it always defaulted to "unknown",
causing all observations to be recorded as "tool_complete" —
PreToolUse events were never distinguished from PostToolUse.
Fix: capture $1 as HOOK_PHASE and pass it to Python via env var.
This also fixes TIMESTAMP export in the .cursor copy where inline
`VAR=val cmd` syntax didn't propagate to the python subprocess.
The inline environment variable syntax `TIMESTAMP="$timestamp" echo ...`
does not work correctly because:
1. The pipe creates a subshell that doesn't inherit the variable
2. The environment variable is set for echo, not for the piped python
Fixed by using `export` and separating the commands:
- export TIMESTAMP="$timestamp"
- echo "$INPUT_JSON" | python3 -c "..."
This ensures the TIMESTAMP variable is available to the python subprocess.
Fixes#227
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add try-catch around readFileSync in validate-agents, validate-commands,
validate-skills to handle TOCTOU races and file read errors
- Add validate-hooks.js and all test suites to package.json test script
(was only running 4/5 validators and 0/4 test files)
- Fix shell variable injection in observe.sh: use os.environ instead of
interpolating $timestamp/$OBSERVATIONS_FILE into Python string literals
- Fix $? always being 0 in start-observer.sh: capture exit code before
conditional since `if !` inverts the status
- Add OLD_VERSION validation in release.sh and use pipe delimiter in sed
to avoid issues with slash-containing values
- Add jq dependency check in evaluate-session.sh before parsing config
- Sync .cursor/ copies of all modified shell scripts
* fix: resolve multiple reported issues (#205, #182, #188, #172, #173)
- fix(observe.sh): replace triple-quote JSON parsing with stdin pipe to
prevent ~49% parse failures on payloads with quotes/backslashes/unicode
- fix(hooks.json): correct matcher syntax to use simple tool name regexes
instead of unsupported logical expressions; move command/path filtering
into hook scripts; use exit code 2 for blocking hooks
- fix(skills): quote YAML descriptions containing colons in 3 skill files
and add missing frontmatter to 2 skill files for Codex CLI compatibility
- feat(rules): add paths: filters to all 15 language-specific rule files
so they only load when working on matching file types
- fix(agents): align model fields with CONTRIBUTING.md recommendations
(opus for planner/architect, sonnet for reviewers/workers, haiku for
doc-updater)
* ci: use AgentShield GitHub Action instead of npx
Switch from npx ecc-agentshield to uses: affaan-m/agentshield@v1
for proper GitHub Action demo and marketplace visibility.
Fixes#113
The instinct commands referenced hardcoded paths that only work with
manual installation (~/.claude/skills/...). When installed as a plugin,
files are at ~/.claude/plugins/cache/.../skills/...
Changes:
- Updated instinct-status, instinct-import, evolve commands to use
${CLAUDE_PLUGIN_ROOT} with fallback to manual path
- Updated observe.sh hook documentation with both install methods
- Updated SKILL.md with plugin vs manual installation instructions
- Removed duplicate commands from skills/continuous-learning-v2/commands/
(already exist in commands/)
Users should use ${CLAUDE_PLUGIN_ROOT} in their hooks config when
installed as a plugin.