Files
cline/docs/customization/clineignore.mdx
5de79a75d0 docs: add .clineignore hook example (#13649)
* docs: add enforced .clineignore guard plugin example

Adds clineignore-read-files-guard.ts, a beforeTool hook plugin that blocks
read_files, editor, apply_patch, and run_commands calls targeting paths
matching gitignore-style patterns in a workspace .clineignore file, and
protects .clineignore itself from modification. Features it on the
.clineignore docs page as the enforced replacement for the deprecated
built-in feature.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: use a PreToolUse file hook for the enforced .clineignore example

Replaces the plugin-based guard with a PreToolUse hook script that works in
the VS Code extension today (.clinerules/hooks/PreToolUse plus the Enable
Hooks setting) as well as the CLI (.cline/hooks/PreToolUse.sh). The script
handles both hook payload shapes, blocks read_files/editor/apply_patch/
run_commands calls matching .clineignore patterns, and protects .clineignore
itself from modification.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: canonicalize paths in .clineignore guard hook

Lexically collapse '.', '..', and empty segments before the ignore match
and the .clineignore self-protection check, closing bypasses via
noncanonical paths like ./.clineignore, secrets/../.env, or
/root/./file (Greptile review finding on #13649).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: note symlink limitation in .clineignore guard hook docs

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-02 19:48:49 -07:00

362 lines
14 KiB
Plaintext

---
title: ".clineignore (deprecate soon)"
sidebarTitle: ".clineignore (deprecate soon)"
description: "Control which files and directories Cline can access in your project. This feature will be deprecated soon."
---
<Warning>
**`.clineignore` will be deprecated soon.**
`.clineignore` filters what Cline loads automatically, but it is not a security or access-control boundary — ignored files can still be read via explicit `@` mentions or shell commands. We're moving away from it as a supported feature.
</Warning>
## Enforcing .clineignore with a Hook
While `.clineignore` is being phased out as a built-in feature, you can keep using the same file — and get a **stronger, enforced** restriction than the original ever provided — with a `PreToolUse` hook. The original `.clineignore` only filtered automatic context loading; the hook script below actively **blocks** the tool call whenever a file read (`read_files`), edit (`editor`, `apply_patch`), or shell command (`run_commands`) targets a file matching a `.clineignore` pattern.
### Install in the VS Code extension
Hooks in the extension live in `.clinerules/hooks/` (workspace) or `~/Documents/Cline/Hooks/` (global), must be named exactly after their event with no file extension, and need a shebang and the executable bit:
```bash
mkdir -p .clinerules/hooks
curl -o .clinerules/hooks/PreToolUse https://raw.githubusercontent.com/cline/cline/main/sdk/examples/hooks/PreToolUse_ClineignoreGuard.sh
chmod +x .clinerules/hooks/PreToolUse
```
Then check **Enable Hooks** in Cline's feature settings.
### Install in the CLI
The CLI discovers hooks from `.cline/hooks/` in the workspace (or `~/.cline/hooks/` globally, or a custom `--hooks-dir`):
```bash
mkdir -p .cline/hooks
curl -o .cline/hooks/PreToolUse.sh https://raw.githubusercontent.com/cline/cline/main/sdk/examples/hooks/PreToolUse_ClineignoreGuard.sh
chmod +x .cline/hooks/PreToolUse.sh
```
### List the files to protect
Add a `.clineignore` file at your workspace root. Patterns use `.gitignore` syntax — directories, globs, and `!` negations all work:
```text
# .clineignore
.env
.env.*
secrets/
*.pem
```
When Cline tries to touch a matching file, the hook cancels the tool call before it runs — the file is never accessed, and the current task run stops. In the extension you'll see the `PreToolUse` hook row followed by an **Aborted** status; in the CLI the run ends. Send a follow-up message to continue the conversation.
The hook records the block reason in its output:
```json
{
"cancel": true,
"errorMessage": "Blocked read_files: .env matched a .clineignore pattern, so Cline may not access it. Update .clineignore if access should be allowed."
}
```
### The hook script
The script handles both hook payload shapes (the extension's `preToolUse.parameters` and the CLI's `tool_call.input`), evaluates paths with real gitignore semantics via `git check-ignore` scoped to `.clineignore` alone, and also blocks attempts to modify `.clineignore` itself so the agent cannot un-ignore files. It requires `jq` and `git` — though `git` is used purely as a pattern matcher, so your workspace does not need to be a git repository.
<Accordion title="PreToolUse_ClineignoreGuard.sh (full script)">
```bash
#!/usr/bin/env bash
# Cline Hook: PreToolUse (.clineignore guard)
# Blocks file reads, edits, and shell commands whose paths match
# gitignore-style patterns listed in <workspace>/.clineignore.
#
# Works with both file-hook payload shapes:
# - VS Code extension hooks (.clinerules/hooks/PreToolUse):
# .preToolUse.toolName + .preToolUse.parameters (values JSON-stringified)
# - CLI / SDK file hooks (.cline/hooks/PreToolUse.sh):
# .tool_call.name + .tool_call.input
#
# Install (VS Code extension — name must be exactly "PreToolUse"):
# mkdir -p .clinerules/hooks
# cp PreToolUse_ClineignoreGuard.sh .clinerules/hooks/PreToolUse
# chmod +x .clinerules/hooks/PreToolUse
# ...and check "Enable Hooks" in Cline's feature settings.
#
# Install (CLI):
# mkdir -p .cline/hooks
# cp PreToolUse_ClineignoreGuard.sh .cline/hooks/PreToolUse.sh
# chmod +x .cline/hooks/PreToolUse.sh
#
# Then list the files to protect in a .clineignore file at your workspace
# root using .gitignore syntax (directories, globs, and ! negations work).
#
# Requires: jq, git (used only as a pattern matcher; the workspace does not
# need to be a git repository).
set -eu
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name // .preToolUse.toolName // ""')
# Only guard tools that read or write files or run shell commands.
case "$tool" in
read_files|editor|apply_patch|run_commands) ;;
*) echo '{"cancel": false}'; exit 0 ;;
esac
root=$(echo "$input" | jq -r '.workspaceRoots[0] // empty')
[ -n "$root" ] || root="$PWD"
ignore_file="$root/.clineignore"
if [ ! -f "$ignore_file" ]; then
echo '{"cancel": false}'
exit 0
fi
# Normalize the tool input: prefer the rich CLI shape, fall back to the
# extension's stringified parameters (dejson re-parses those values).
jq_prelude='
def dejson: if type == "string" then (fromjson? // .) else . end;
((.tool_call.input // .preToolUse.parameters // {}) | dejson) as $in |'
# Collect every candidate path from the tool input.
case "$tool" in
apply_patch)
# apply_patch carries its paths inside the patch body headers.
paths=$(echo "$input" | jq -r "$jq_prelude"'
if ($in | type) == "string" then $in
elif ($in | type) == "object" then ($in.input // "" | dejson)
else "" end | strings' \
| sed -n \
-e 's/^\*\*\* Add File: //p' \
-e 's/^\*\*\* Update File: //p' \
-e 's/^\*\*\* Delete File: //p' \
-e 's/^\*\*\* Move to: //p')
;;
run_commands)
# Conservative shell guard: treat every token of every command as a
# candidate path. Catches straightforward access like `cat .env`
# without attempting full shell parsing.
paths=$(echo "$input" | jq -r "$jq_prelude"'
(if ($in | type) == "string" then [$in]
elif ($in | type) == "array" then $in
elif ($in | type) == "object" then
[($in.commands // $in.command // $in.cmd // empty) | dejson] | flatten
else [] end)
| .[]
| if type == "object"
then ((.command // empty), ((.args // []) | dejson | .[]?))
else . end
| strings' \
| tr -s '[:space:];|&()<>' '\n' \
| sed -e "s/^[\"']*//" -e "s/[\"']*\$//" \
| grep -v '^-' | grep -v '^$' || true)
;;
*)
# read_files / editor accept a few input shapes:
# {files: [{path}]}, {path}, plain strings, string arrays, aliases...
paths=$(echo "$input" | jq -r "$jq_prelude"'
(if ($in | type) == "string" then [$in]
elif ($in | type) == "array" then $in
elif ($in | type) == "object" then
[$in.path?, $in.file_path?, $in.filePath?]
+ (($in.files // []) | dejson | if type == "array" then . else [.] end)
+ (($in.paths // []) | dejson | if type == "array" then . else [.] end)
+ (($in.file_paths // []) | dejson | if type == "array" then . else [.] end)
else [] end)
| .[]
| dejson
| if type == "object" then (.path // .file_path // .filePath // empty) else . end
| strings')
;;
esac
# Lexically collapse ".", "..", and empty segments of an absolute path, so
# noncanonical forms like ./.clineignore or secrets/../.env cannot slip
# past the checks below.
normalize_abs() {
local out="" seg rest="$1/"
while [ -n "$rest" ]; do
seg="${rest%%/*}"
rest="${rest#*/}"
case "$seg" in
""|".") ;;
"..") out="${out%/*}" ;;
*) out="$out/$seg" ;;
esac
done
printf '%s\n' "${out:-/}"
}
# Make paths canonical and workspace-relative. Paths that resolve outside
# the workspace are not covered by .clineignore, so they pass through.
root=$(normalize_abs "$root")
rel_paths=""
while IFS= read -r p; do
[ -n "$p" ] || continue
case "$p" in
/*) ;;
*) p="$root/$p" ;;
esac
p=$(normalize_abs "$p")
case "$p" in
"$root"/*) rel_paths+="${p#"$root"/}"$'\n' ;;
esac
done <<< "$paths"
if [ -z "$rel_paths" ]; then
echo '{"cancel": false}'
exit 0
fi
# The guard is only as strong as the ignore file itself: protect
# .clineignore from modification so the agent cannot un-ignore files.
if [ "$tool" != "read_files" ] && printf '%s' "$rel_paths" | grep -qx '\.clineignore'; then
jq -n --arg tool "$tool" \
'{cancel: true, errorMessage: "Blocked \($tool): modifying .clineignore is not allowed. Update it yourself if a file should be un-ignored."}'
exit 0
fi
# Evaluate the paths against .clineignore with real gitignore semantics via
# `git check-ignore`. Running it inside an empty scratch repo scopes the
# check to .clineignore alone -- the workspace's own .gitignore files are
# never consulted, and this works even outside a git repository.
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
git init -q "$scratch"
blocked=$(printf '%s' "$rel_paths" \
| git -C "$scratch" -c core.excludesFile="$ignore_file" check-ignore --stdin --no-index 2>/dev/null \
| paste -sd, -)
if [ -n "$blocked" ]; then
jq -n --arg tool "$tool" --arg files "$blocked" \
'{cancel: true, errorMessage: "Blocked \($tool): \($files) matched a .clineignore pattern, so Cline may not access it. Update .clineignore if access should be allowed."}'
else
echo '{"cancel": false}'
fi
```
</Accordion>
The source lives at [`sdk/examples/hooks/PreToolUse_ClineignoreGuard.sh`](https://github.com/cline/cline/blob/main/sdk/examples/hooks/PreToolUse_ClineignoreGuard.sh).
<Note>
This hook covers only part of what `.clineignore` did — and enforces more than it ever did:
- It **blocks file reads, edits, and shell commands** — it does **not** filter search or file-listing results, so it is an access-control boundary rather than a context-reduction tool (the original `.clineignore` was the reverse).
- A cancelled tool call stops the current task run with the reason shown; send a follow-up message to continue the conversation.
- The shell guard is a conservative token check, not a full shell parser: it catches straightforward access like `cat .env`, but a sufficiently creative command could still slip through. Paths are canonicalized lexically, but symlinks are not resolved — a pre-existing symlink aliasing an ignored file is not caught, so add such aliases to `.clineignore` too.
- In the CLI, hooks are disabled in `--yolo` mode; use `--act` or `--plan`.
</Note>
---
<Info>
The rest of this page is the original `.clineignore` reference, retained for existing users while the feature is phased out.
</Info>
The `.clineignore` file tells Cline which files and directories to skip when analyzing your codebase. It works like `.gitignore`: create a file named `.clineignore` in your project root, add patterns for files you want excluded, and Cline will ignore them.
## Why It Matters
Without a `.clineignore`, Cline may load your entire project into context, including dependencies, build artifacts, and generated files. This wastes tokens, increases costs, and can push useful context out of the window.
Adding a `.clineignore` can cut your starting context from 200k+ tokens to under 50k. That means faster responses, lower costs, and the ability to use smaller, cheaper models effectively.
## Creating a .clineignore
Create a file named `.clineignore` in your project root:
```text
# Dependencies
node_modules/
**/node_modules/
# Build outputs
/build/
/dist/
/.next/
/out/
# Testing artifacts
/coverage/
# Environment variables
.env
.env.*
# Large data files
*.csv
*.xlsx
*.sqlite
# Generated/minified code
*.min.js
*.map
```
## Pattern Syntax
`.clineignore` uses the same pattern syntax as `.gitignore`:
| Pattern | Matches |
|---------|---------|
| `node_modules/` | The `node_modules` directory |
| `**/node_modules/` | `node_modules` at any depth |
| `*.csv` | All CSV files |
| `/build/` | The `build` directory at the project root only |
| `*.env.*` | Files like `.env.local`, `.env.production` |
| `!important.csv` | Exception: do not ignore this file |
Lines starting with `#` are comments. Blank lines are ignored.
## What to Exclude
Start with these categories and adjust for your project:
**Almost always exclude:**
- Package manager directories (`node_modules/`, `vendor/`, `.venv/`)
- Build outputs (`dist/`, `build/`, `.next/`, `out/`)
- Coverage reports (`coverage/`)
- Lock files if large (`package-lock.json`, `yarn.lock`)
**Exclude if present:**
- Large data files (`.csv`, `.xlsx`, `.sqlite`, `.parquet`)
- Binary assets (images, fonts, videos)
- Generated code (API clients, protobuf outputs, minified bundles)
- Environment files with secrets (`.env`, `.env.local`)
**Keep accessible:**
- Source code you actively work on
- Configuration files Cline needs to understand (`tsconfig.json`, `package.json`)
- Documentation and READMEs
- Test files (Cline often needs these for context)
## How It Works
When Cline scans your project to build context, it checks each file path against your `.clineignore` patterns. Matching files are excluded from:
- The file listing Cline sees when starting a task
- Automatic context gathering during conversations
- Search results when Cline looks for relevant code
As noted above, explicit [@ mentions](/core-workflows/working-with-files) still bypass these rules — for example, `@/node_modules/some-package/index.js` reads that file even though `node_modules/` is ignored. Ignore rules control automatic loading, not explicit access.
<Note>
`.clineignore` is separate from `.gitignore`. Files tracked by Git but irrelevant to Cline (like large test fixtures or data files) should go in `.clineignore` even if they're not in `.gitignore`.
</Note>
## Tips
- Check your token usage in the task header after adding a `.clineignore`. The difference is often dramatic.
- If Cline seems to be missing context about a file, check whether it's being excluded by your ignore patterns.
- For monorepos or multi-root workspaces, each workspace root can have its own `.clineignore`. See [Multi-Root Workspaces](/features/multiroot-workspace) for details.
## Related
- [Cline Rules](/customization/cline-rules) - Define persistent instructions for Cline
- [Task Management](/core-workflows/task-management#context-window) - Understand how context windows work
- [Auto-Compact](/features/auto-compact) - Automatic context compression during long tasks