Skip to content

feat: add clineignore plugin#223

Open
saoudrizwan wants to merge 2 commits into
mainfrom
saoudrizwan/clineignore-plugin
Open

feat: add clineignore plugin#223
saoudrizwan wants to merge 2 commits into
mainfrom
saoudrizwan/clineignore-plugin

Conversation

@saoudrizwan

Copy link
Copy Markdown
Contributor

What

Adds a clineignore plugin: the plugin successor to the built-in .clineignore support that only ever existed in the legacy VS Code extension (ClineIgnoreController) and was never migrated to the SDK/CLI. With .clineignore now marked as deprecated in the docs (cline/cline#12410), this gives users a real replacement they can install with cline plugin install clineignore.

It reads a .clineignore file at the workspace root (standard .gitignore syntax via the ignore npm package, the same library the legacy controller used) and registers a beforeTool hook that:

  • skips read_files / editor / apply_patch calls when any requested path matches an ignore pattern
  • skips run_commands calls when a file-reading command (cat, less, more, head, tail, grep, awk, sed, plus the PowerShell equivalents get-content, gc, type, select-string, sls) targets an ignored path

It also registers a system prompt rule (when .clineignore exists at session start) so the model knows blocked paths are intentional and that it should work around them or ask the user, mirroring the clineIgnoreInstructions prompt section the legacy extension injected.

Legacy parity decisions

I went through the legacy implementation enforcement point by enforcement point and deliberately kept its semantics where they were defensible:

  • !include <file> directive support (pulls extra pattern files, resolved relative to the workspace root; missing includes silently skipped)
  • the .clineignore file itself is always treated as ignored
  • paths outside the workspace root fail open (allowed), same as legacy validateAccess
  • the model-facing error text closely echoes the legacy clineIgnoreError response ("Access to X is blocked by the .clineignore file...") so models behave the same way they did against the legacy extension
  • command scanning is the same base-command allowlist heuristic, with one improvement: commands are split on |, ||, &&, and ; so each pipeline/sequence segment gets its base command checked (legacy only looked at the first token of the whole string, so ls && cat .env slipped through)

Instead of legacy's chokidar watcher, the ignore file is re-stat'd per hook call and the parsed matcher is cached by mtime+size. Same live-reload user experience (editing .clineignore takes effect immediately, mid session), no watcher lifecycle to manage, and plugins have no host-provided file watching API anyway.

Deliberately descoped from v1: filtering ignored files out of search_codebase results. The search tool returns a preformatted string per query, so filtering means fragile string parsing in an afterTool hook. The README documents this as a known hole. Also not covered, same as legacy: shell redirection/subshells/xargs, and quote-aware tokenization. The README is explicit that this is context hygiene, not a security boundary, which matches the reasoning in the deprecation docs commit.

The sandbox interop bug (debugging journey)

The first version passed a 22-case smoke test under plain bun run and typechecked against published @cline/core, then silently failed to block anything in a real installed session. No error anywhere: the fail-open catch in the loader swallowed it, and console.warn from sandboxed plugins goes nowhere.

Debugging recipe that found it: instrument the installed copy under .cline/plugins/_installed/.../package/index.ts with appendFileSync probes (module scope, setup, hook body, catch blocks) and rerun the session. That showed the plugin loaded, setup got the right workspace root, beforeTool fired... and the matcher constructor threw (0 , _ignore.default) is not a function.

Root cause: the plugin sandbox's transpiled module interop delivers the ignore package (dual CJS/ESM) as a namespace object { default: factory, isPathValid } where the transpiled default-import access expects the factory itself. Under plain bun the same import IS the factory, which is exactly why unit tests can't catch this class of bug. Fix is a runtime unwrap:

const createIgnore = typeof ignore === "function" ? ignore : ignore.default;

Anyone writing a sandboxed plugin with a CJS/dual-package dependency will hit this same landmine; worth keeping in mind for other plugins in this repo.

One more behavior found only in the live run: a batched read_files call mixing blocked and allowed paths skips the whole call (beforeTool skip is all-or-nothing), so the reason string now tells the model to re-request allowed files in a separate call, which the model did correctly on the next turn.

How to test

mkdir /tmp/ws && cd /tmp/ws
printf '.env\n' > .clineignore
printf 'SECRET_KEY=hunter2\n' > .env
cline plugin install /path/to/plugins/clineignore --cwd . --force
cline "Use the read_files tool to read .env, then stop"
# read is skipped with the .clineignore error; also try: cline "run cat .env"

Verified: npm run validate passes, tsc --noEmit against published @cline/core types, a 22-case smoke suite driving the hook directly (ignored/allowed/outside-workspace paths, !include, structured + string + pipeline command inputs, live reload after editing .clineignore, no-op when no ignore file exists), and the full CLI e2e above on cline 3.0.46 (both the failing before-fix run and the passing after-fix run).

Marketplace entry: cline/marketplace PR adds registry/plugins/clineignore. Follow-up for cline/cline: point the .clineignore deprecation docs at this plugin instead of the gitignore-read-files-guard example. Note the new VS Code/JetBrains extension can't run SDK plugins yet, so this replacement currently covers CLI/SDK/Kanban only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant