mirror of
https://github.com/cline/cline.git
synced 2026-08-30 17:20:20 +08:00
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:
@@ -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 project’s 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. |
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user