11 Commits

  • fix: stability batch — hook stdin truncation, Codex exa TOML, Stop hook JSON, GateGuard repetition (#2227)
    * fix(hooks): fail open on oversized stdin instead of echoing truncated JSON (#2222)
    
    run-with-flags.js capped stdin at 1MB but every fallthrough path still
    echoed the truncated string to stdout. The harness parses hook stdout as
    JSON, got a document cut mid-stream, and blocked the tool call — so any
    Edit/Write with a >1MB hook payload was permanently blocked by every
    registered pre-write hook, before ECC_HOOK_PROFILE / ECC_DISABLED_HOOKS
    gating could run.
    
    - Exit 0 with empty stdout (no opinion) when the stdin cap trips, before
      any echo or gating logic.
    - Flush stdout via write callback before process.exit: exiting right
      after stdout.write() dropped everything past the ~64KB pipe buffer,
      cutting even sub-cap pass-through payloads mid-JSON.
    
    Regression tests cover the enabled, disabled, and missing-arg paths for
    oversized payloads plus full echo of sub-cap >64KB payloads.
    
    * fix(codex): stop emitting invalid exa url entry, align merge with connector policy (#2224)
    
    The Codex MCP merge declared exa with a url key, but Codex's
    [mcp_servers.*] TOML schema is stdio-only — the url key makes the
    entire config.toml fail to load, bricking both the codex CLI and the
    desktop app. Every install/update re-injected the line because the
    urlEntry branch treated the broken entry as present.
    
    - ECC_SERVERS now emits only the current default set per
      docs/MCP-CONNECTOR-POLICY.md: chrome-devtools (stdio, command/args).
      Retired servers (supabase, playwright, context7, exa, github, memory,
      sequential-thinking) are never re-emitted; existing user-managed
      entries are untouched.
    - The merge now repairs the exact ECC-emitted broken form (url-only
      exa entry) on every run so re-running the installer fixes broken
      configs instead of preserving them. User stdio exa entries
      (command + mcp-remote) are left alone.
    - check-codex-global-state.sh requires chrome-devtools instead of the
      retired set, and flags url-only exa entries with a repair hint.
    
    Tests cover repair, re-run idempotence, stdio-entry preservation, and
    no-retired-server emission in add, update, dry-run, and disabled modes.
    
    * fix(hooks): never echo truncated stdin from Stop hooks (#2090)
    
    Stop hooks follow the ECC pass-through convention (echo stdin on
    stdout), but every echoing Stop hook capped stdin and echoed the capped
    string. The Stop payload carries last_assistant_message, so a long
    final assistant message produced a JSON document cut mid-stream on
    stdout, which the harness reports as 'Stop hook error: JSON validation
    failed' across the whole Stop chain.
    
    Reproduced: a Stop payload with a >64KB last_assistant_message run
    through run-with-flags + cost-tracker emitted exactly 65536 bytes of
    invalid JSON (cost-tracker capped stdin at 64KB — far below realistic
    Stop payloads).
    
    - cost-tracker: raise the cap to 1MB (matching all other hooks) and
      suppress the pass-through echo when stdin was truncated.
    - check-console-log, stop-format-typecheck, desktop-notify: suppress
      the echo when stdin was truncated; flush stdout before process.exit
      so sub-cap payloads are not cut at the ~64KB pipe buffer.
    - All hooks keep exiting 0 (fail-open); diagnostics go to stderr.
    
    New stop-hooks-stdout test asserts the contract for every registered
    Stop hook: stdout is empty or valid JSON, exit code 0 — for realistic
    100KB payloads and oversized >1MB payloads, via the production runner
    and via direct invocation. Updated the old hooks.test.js case that
    codified the truncated-echo behavior.
    
    * fix(hooks): dampen GateGuard fact-force repetition in long sessions (#2142)
    
    In long autonomous sessions the fact-force gate produced 10+
    near-identical 'state facts -> blocked -> restate -> retry' blocks in
    one context window, which measurably raises the odds of the model
    collapsing into a degenerate single-token repetition loop.
    
    - Track a per-session fact_force_denials counter in GateGuard state
      (merged max across concurrent writers, reset with the session, robust
      to malformed on-disk values).
    - The first GATEGUARD_FACT_FORCE_FULL_DENIALS denials (default 3) keep
      the full four-fact block; later denials emit a condensed single-line
      message that carries the denial ordinal, so consecutive denials are
      structurally different and never textually identical.
    - True retries of the same target remain allowed without re-prompting
      (unchanged). Destructive-Bash and routine-Bash gates are unchanged,
      as are the ECC_GATEGUARD=off / ECC_DISABLED_HOOKS escape hatches.
    
    Eight new tests cover budget counting, condensed format, ordinal
    advancement, retry pass-through, env tuning, malformed state, MultiEdit
    dampening, and destructive-gate exemption.
    
    * fix(hooks): keep security hooks able to block on oversized stdin (#2222)
    
    Refine the truncation fail-open: instead of skipping the hook entirely,
    the runner now suppresses only its own raw-echo when stdin was
    truncated. The hook still executes and receives the truncated flag
    (run() context / ECC_HOOK_INPUT_TRUNCATED), so config-protection keeps
    blocking truncated protected-config payloads (its test requires exit 2)
    while pass-through hooks fail open with empty stdout as before.
    
    * style: apply repo formatter to touched hook files
  • fix: add cwd to prettier hook, consistent process.exit(0), and stdout pass-through
    - post-edit-format.js: add cwd based on file directory so npx resolves
      correct local prettier binary
    - post-edit-typecheck.js, post-edit-format.js: replace console.log(data)
      with process.stdout.write(data) to avoid trailing newline corruption
    - Add process.exit(0) to 4 hooks for consistent termination
      (check-console-log, post-edit-console-warn, post-edit-format,
      post-edit-typecheck)
    - run-all.js: switch from execSync to spawnSync so stderr is visible
      on the success path (hook warnings were silently discarded)
    - Add 21 tests: cwd verification, process.exit(0) checks, exact
      stdout pass-through, extension edge cases, exclusion pattern
      matching, threshold boundary values (630 → 651)
  • fix: eliminate command injection in hooks, fix pass-through newline corruption, add 8 tests
    Replace shell: true with npx.cmd on Windows in post-edit-format.js and
    post-edit-typecheck.js to prevent command injection via crafted file paths.
    Replace console.log(data) with process.stdout.write(data) in
    check-console-log.js to avoid appending extra newlines to pass-through data.
  • fix: 3 bugs fixed, stdin encoding hardened, 37 CI validator tests added
    Bug fixes:
    - utils.js: glob-to-regex conversion now escapes all regex special chars
      (+, ^, $, |, (), {}, [], \) before converting * and ? wildcards
    - validate-hooks.js: escape sequence processing order corrected —
      \\\\ now processed before \\n and \\t to prevent double-processing
    - 6 hooks: added process.stdin.setEncoding('utf8') to prevent
      multi-byte UTF-8 character corruption at chunk boundaries
      (check-console-log, post-edit-format, post-edit-typecheck,
      post-edit-console-warn, session-end, evaluate-session)
    
    New tests (37):
    - CI validator test suite (tests/ci/validators.test.js):
      - validate-agents: 9 tests (real project, frontmatter parsing,
        BOM/CRLF, colons in values, missing fields, non-md skip)
      - validate-hooks: 13 tests (real project, invalid JSON, invalid
        event types, missing fields, async/timeout validation, inline JS
        syntax, array commands, legacy format)
      - validate-skills: 6 tests (real project, missing SKILL.md, empty
        files, non-directory entries)
      - validate-commands: 5 tests (real project, empty files, non-md skip)
      - validate-rules: 4 tests (real project, empty files)
    
    Total test count: 228 (up from 191)
  • fix: use readFile utility in hooks and add pattern type safety
    - Replace raw fs.readFileSync with readFile() from utils in
      check-console-log.js and post-edit-console-warn.js to eliminate
      TOCTOU race conditions (file deleted between existsSync and read)
    - Remove redundant existsSync in post-edit-format.js (exec already
      handles missing files via its catch block)
    - Resolve path upfront in post-edit-typecheck.js before tsconfig walk
    - Add type guard in getGitModifiedFiles() to skip non-string and
      empty patterns before regex compilation
  • fix: path traversal in install.sh, error logging in hooks
    - Validate language names in install.sh to prevent path traversal via
      malicious args like ../../etc (only allow [a-zA-Z0-9_-])
    - Replace silent catch in check-console-log.js with stderr logging so
      hook failures are visible to the user for debugging
    - Escape backticks in session-end.js user messages to prevent markdown
      structure corruption in session files
  • fix: harden error handling, fix TOCTOU races, and improve test accuracy
    Core library fixes:
    - session-manager.js: wrap all statSync calls in try-catch to prevent
      TOCTOU crashes when files are deleted between readdir and stat
    - session-manager.js: use birthtime||ctime fallback for Linux compat
    - session-manager.js: remove redundant existsSync before readFile
    - utils.js: fix findFiles TOCTOU race on statSync inside readdir loop
    
    Hook improvements:
    - Add 1MB stdin buffer limits to all PostToolUse hooks to prevent
      unbounded memory growth from large payloads
    - suggest-compact.js: use fd-based atomic read+write for counter file
      to reduce race window between concurrent invocations
    - session-end.js: log when transcript file is missing, check
      replaceInFile return value for failed timestamp updates
    - start-observer.sh: log claude CLI failures instead of silently
      swallowing them, check observations file exists before analysis
    
    Test fixes:
    - Fix blocking hook tests to send matching input (dev server command)
      and expect correct exit code 2 instead of 1
  • fix: improve error handling, fix bugs, and optimize core libraries
    utils.js:
    - Fix countInFile: enforce global flag on regex to prevent silent
      under-counting (match() without /g returns only first match)
    - Add 5s timeout to readStdinJson to prevent hooks hanging forever
    - Handle EEXIST race condition in ensureDir
    - Pre-compile regex patterns in getGitModifiedFiles to avoid N*M
      compilations and catch invalid patterns before filtering
    - Add JSDoc documentation to all improved functions
    
    session-manager.js:
    - Fix getSessionById triple file read: pass pre-read content to
      getSessionStats instead of re-reading from disk
    - Allow getSessionStats to accept content string directly
    
    session-aliases.js:
    - Wrap temp file cleanup in try/catch to prevent cascading errors
    
    check-console-log.js:
    - Refactor to use shared utils (isGitRepo, getGitModifiedFiles, log)
      instead of raw execSync calls
    - Add exclusion patterns for test files, config files, and scripts/
      where console.log is intentional
    
    session-end.js:
    - Log count of skipped unparseable transcript lines for diagnostics
    
    suggest-compact.js:
    - Guard against NaN from corrupted counter files
    
    package-manager.js:
    - Remove dead fallbackOrder parameter (unused after #162 fix)
  • fix: resolve ESLint errors and update tests for project-name fallback
    - Fix 16 ESLint no-unused-vars errors across hook scripts and tests
    - Add eslint-disable comment for intentional control-regex in ANSI stripper
    - Update session file test to use getSessionIdShort() instead of hardcoded 'default'
      (reflects PR #110's project-name fallback behavior)
    - Add marketing/ to .gitignore (local drafts)
    - Add skill-create-output.js (terminal output formatter)
    
    All 69 tests now pass. CI should be green.
  • Fix: Move Stop hook inline code to separate script file
    Fixes #78
    
    ## Problem
    The Stop hook used inline JavaScript code with `node -e`, which caused
    shell syntax errors on macOS/zsh due to special characters (parentheses,
    braces, arrow functions) being misinterpreted by the shell.
    
    Error message:
    /bin/sh: -c: line 0: syntax error near unexpected token \`('
    
    ## Solution
    - Created scripts/hooks/check-console-log.js with the hook logic
    - Updated hooks/hooks.json to reference the external script
    - This follows the same pattern as other hooks in the plugin
    
    ## Benefits
    - Fixes shell compatibility issues across different environments
    - Improves code maintainability (separate, well-documented script)
    - Follows plugin's own best practices
    - Makes the code easier to test and debug
    
    ## Testing
    Tested on macOS with zsh - no more syntax errors.
    The hook still functions correctly to detect console.log statements.