docs(sdk): add env-blocker plugin example (#11192)

* docs(sdk): add env-blocker plugin example

Adds a beforeTool hook plugin that deterministically blocks the agent
from reading .env secret files via read_files, editor, or run_commands
(e.g. cat .env), while leaving .env.example/.sample/.template readable.
Demonstrates moving a security policy out of an AGENTS.md rule (a
suggestion the model can ignore) and into the execution path.

* docs(sdk): install env-blocker globally in usage examples

A secret-protection guard is most useful applied to every project, so
drop the --cwd . project-scoped install in favor of the global default.

* docs(sdk): trim env-blocker usage docs

* docs(sdk): limit env-blocker to read paths only

It is a read blocker, so only guard read_files and run_commands.
Drop the editor case (and with it the symmetric apply_patch concern),
keeping the example focused and simple.

* docs(sdk): rename env-blocker helpers for readability

collectPaths -> extractFilePaths, collectCommands -> extractShellCommands
so the beforeTool call sites read clearly at a glance.

* docs(sdk): rename commandTouchesEnv to commandReadsEnv

* docs(sdk): drop console.error from env-blocker hook
This commit is contained in:
Saoud Rizwan
2026-06-01 12:12:16 -07:00
committed by GitHub
parent dc2c662de6
commit 05042d3ff7
2 changed files with 119 additions and 0 deletions
+1
View File
@@ -19,6 +19,7 @@ What a plugin can do:
| [background-terminal.ts](./background-terminal.ts) | Detached shell jobs with persisted logs and session steering | Registers `start_background_command`, `get_background_command`, and `delete_background_command` so agents can launch long-running shell commands, poll stdout/stderr tails, clean up job metadata, and receive completion summaries as steer messages. |
| [automation-events.ts](./automation-events.ts) | Plugin-emitted automation events | Registers a normalized `local.plugin_event` automation event type and, when `CLINE_LOCAL_EVENT_INTERVAL_MS` is set, periodically emits demo events into Cline automation. |
| [gitignore-read-files-guard.ts](./gitignore-read-files-guard.ts) | Runtime hook policy for workspace `.gitignore` boundaries | Uses `beforeTool` to inspect `read_files`, `editor`, and `apply_patch` requests and skips them when target paths match workspace `.gitignore` rules, preventing ignored files from being read or modified. |
| [env-blocker.ts](./env-blocker.ts) | Deterministic secret protection via `beforeTool` | Uses `beforeTool` to block `read_files` and `run_commands` (e.g. `cat .env`) calls that read `.env` secret files, while leaving `.env.example`/`.env.sample`/`.env.template` readable. A hard guarantee where an AGENTS.md rule is only a suggestion. |
| [web-search.ts](./web-search.ts) | `web_search` tool backed by an Exa API key | Adds a `web_search` tool that queries Exa for current public web results, with optional result limits, domain filters, recency windows, and country localization. Requires `EXA_API_KEY`. |
| [typescript-lsp/](./typescript-lsp/) | `goto_definition` tool powered by the TypeScript Language Service | Adds `goto_definition(file, line)` for TypeScript/JavaScript projects. It loads the target projects own TypeScript version, finds identifiers on a line, and resolves definitions through imports, re-exports, aliases, and other language-service semantics. |
| [agents-squad/](./agents-squad/) | Multi-agent team — spin up subagents with their own models and personalities | Adds tools for starting, messaging, polling, and coordinating background subagents. It includes bundled agent presets, skill discovery/loading, and a shared handoff store for passing notes between subagents in the same conversation. |
+118
View File
@@ -0,0 +1,118 @@
/**
* Env Blocker Plugin Example
*
* A rule in AGENTS.md / .clinerules ("never read .env files") is a suggestion the
* model can ignore. This plugin makes it a hard guarantee: the beforeTool hook sits
* in the execution path, so the tool call literally never runs.
*
* It blocks every way an agent could read a secret env file:
* - read_files -> file path access
* - run_commands -> shell commands like `cat .env` or `source .env.production`
*
* Template files (.env.example, .env.sample, .env.template) stay readable.
*
* CLI usage:
* cline plugin install https://github.com/cline/cline/blob/main/sdk/examples/plugins/env-blocker.ts
* cline -i "Read the .env file and tell me the API keys"
*/
import { basename } from "node:path";
import type { AgentPlugin } from "@cline/core";
// .env.example / .env.sample / .env.template hold placeholders, not secrets.
const TEMPLATE = /\.env\.(example|sample|template)$/i;
/** True for .env, .env.local, .env.production, path/to/.env, etc. (but not templates). */
function isEnvFile(rawPath: string): boolean {
const path = rawPath.trim().replace(/^['"]|['"]$/g, "");
const name = basename(path);
if (TEMPLATE.test(name)) {
return false;
}
return /^\.env(\.|$)/i.test(name);
}
/** True if a shell command reads a secret env file (cat .env, source ./.env, etc.). */
function commandReadsEnv(command: string): boolean {
const tokens = command.match(/[\w./-]*\.env[\w.-]*/gi) ?? [];
return tokens.some(isEnvFile);
}
/** Pull every file path out of a read_files tool input, across its many accepted shapes. */
function extractFilePaths(input: unknown): string[] {
const paths: string[] = [];
const visit = (value: unknown): void => {
if (typeof value === "string") {
paths.push(value);
} else if (Array.isArray(value)) {
value.forEach(visit);
} else if (value && typeof value === "object") {
const record = value as Record<string, unknown>;
if (typeof record.path === "string") {
paths.push(record.path);
}
visit(record.files);
visit(record.file_paths);
visit(record.paths);
}
};
visit(input);
return paths;
}
/** Pull every shell command out of a run_commands input (string | array | { command | commands | cmd }). */
function extractShellCommands(input: unknown): string[] {
if (typeof input === "string") {
return [input];
}
if (Array.isArray(input)) {
return input.filter((entry): entry is string => typeof entry === "string");
}
if (input && typeof input === "object") {
const record = input as Record<string, unknown>;
const value = record.command ?? record.commands ?? record.cmd;
if (typeof value === "string") {
return [value];
}
if (Array.isArray(value)) {
return value.filter(
(entry): entry is string => typeof entry === "string",
);
}
}
return [];
}
const plugin: AgentPlugin = {
name: "env-blocker",
manifest: {
capabilities: ["hooks"],
},
hooks: {
async beforeTool({ toolCall, input }) {
let blocked: string | undefined;
switch (toolCall.toolName) {
case "read_files":
blocked = extractFilePaths(input).find(isEnvFile);
break;
case "run_commands":
blocked = extractShellCommands(input).find(commandReadsEnv);
break;
}
if (!blocked) {
return undefined;
}
return {
skip: true,
reason: `Blocked ${toolCall.toolName}: reading environment secret files (${blocked}) is not permitted. Ask the user for any values you need.`,
};
},
},
};
export { plugin };
export default plugin;