5 Commits

  • fix(hooks): address coderabbit review — use lstatSync for symlink paths
    CodeRabbit major on PR #1898: fs.statSync follows symlinks, so a dangling
    protected symlink (e.g. .eslintrc.js pointing at a missing target) would
    throw ENOENT and be treated as absent — letting an agent "replace" the
    symlink and bypass the protection.
    
    Swap statSync for lstatSync. lstat reports the link node itself regardless
    of whether its target exists, so protected entries that happen to be
    symlinks stay blocked. ENOENT handling is unchanged: only a genuinely
    missing path (no link, no file, no directory) counts as absent.
    
    Add a regression test that creates a dangling symlink at .eslintrc.js and
    verifies the hook still blocks Write. Skips cleanly on platforms/sandboxes
    that disallow symlink creation (EPERM/EACCES).
  • fix(hooks): address greptile review — use statSync for true fail-closed
    Greptile P1 on PR #1898: fs.existsSync internally catches all errors and
    returns false, so the previous try/catch around it was dead code and the
    stated "fail-closed on EACCES" semantics weren't actually delivered. A
    file under a directory with no execute permission would read as absent
    and bypass the guard.
    
    Swap to fs.statSync with explicit ENOENT detection. Only ENOENT flips
    exists to false; every other error code (EACCES, EPERM, ELOOP, etc.)
    leaves exists=true so the modification guard is never silently weakened.
    
    Add a new test "allows first-time creation when the parent directory
    does not exist yet" that exercises the ENOENT path via a non-existent
    parent dir — pins the happy path into the regression suite.
  • fix(hooks): allow first-time creation of protected config files
    The config-protection hook blocks Write/Edit on any basename in the
    PROTECTED_FILES set, regardless of whether the file already exists. The
    hook's stated purpose is to prevent agents from softening rules in an
    existing config — but the same code path also blocks the legitimate
    bootstrap case of scaffolding a linter config into a project that has
    none.
    
    Add an fs.existsSync check inside run(): when the basename matches a
    protected entry and the file does not yet exist on disk, exit 0 and
    let the Write proceed. Keep the exit-2 block for all modifications to
    existing files. Stat errors (EACCES, etc.) fail closed — we treat the
    path as existing so the guard is never silently weakened.
    
    Update the existing "blocks protected config file edits" test to use a
    real temp file so the BLOCK path is still exercised, and add two new
    tests covering:
    
    - first-time creation of eslint.config.mjs is allowed (exit 0, raw
      passthrough, no stderr)
    - Edit against an existing .eslintrc.js is still blocked (exit 2, no
      stdout, BLOCKED message in stderr)
    
    Fixes #1873
  • feat(hooks): add config protection hook to block linter config manipulation (#758)
    * feat(hooks): add config protection hook to block linter config manipulation
    
    Agents frequently modify linter/formatter configs (.eslintrc, biome.json,
    .prettierrc, .ruff.toml, etc.) to make checks pass instead of fixing
    the actual code.
    
    This PreToolUse hook intercepts Write/Edit/MultiEdit calls targeting
    known config files and blocks them with a steering message that directs
    the agent to fix the source code instead.
    
    Covers: ESLint, Prettier, Biome, Ruff, ShellCheck, Stylelint, and
    Markdownlint configs.
    
    Fixes #733
    
    * Address review: fix dead code, add missing configs, export run()
    
    - Removed pyproject.toml from PROTECTED_FILES (was dead code since
      it was also in PARTIAL_CONFIG_FILES). Added comment explaining why
      it's intentionally excluded.
    - Removed PARTIAL_CONFIG_FILES entirely (no longer needed).
    - Added missing ESLint v9 TypeScript flat configs: eslint.config.ts,
      eslint.config.mts, eslint.config.cts
    - Added missing Prettier ESM config: prettier.config.mjs
    - Exported run() function for in-process execution via run-with-flags,
      avoiding the spawnSync overhead (~50-100ms per call).
    
    * Handle stdin truncation gracefully, log warning instead of fail-open
    
    If stdin exceeds 1MB, the JSON would be malformed and the catch
    block would silently pass through. Now we detect truncation and
    log a warning. The in-process run() path is not affected.