-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add gatekeeper plugin for auto-approve safe commands #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
356e841
feat: add gatekeeper plugin for auto-approve safe commands
amondnet c405db1
chore: include gatekeeper dist in git
amondnet a12fc0d
chore: add gatekeeper to release-please config
amondnet 8e6e857
fix: tighten deny patterns to prevent false positives
amondnet 75d0c60
fix(gatekeeper): handle invalid JSON input and unhandled rejections
amondnet 5a7b852
fix(gatekeeper): apply security improvements from AI code review
amondnet 13fbf67
chore(gatekeeper): upgrade PermissionRequest agent model to opus
amondnet abfd897
chore(gatekeeper): revert PermissionRequest agent model to haiku
amondnet 370c582
chore(gatekeeper): enable eslint cache for turbo caching support
amondnet 8fe1b35
fix(gatekeeper): improve Layer 2 security prompt and upgrade to opus
amondnet 224b886
docs(gatekeeper): update README with current config and add reference…
amondnet b30980a
fix(gatekeeper): run DENY before chaining check and fix git push regex
amondnet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "name": "gatekeeper", | ||
| "version": "1.0.0", | ||
| "description": "Two-layer security for Claude Code: auto-approve safe commands, AI-assisted review for the rest", | ||
| "author": { | ||
| "name": "Minsu Lee", | ||
| "url": "https://github.com/amondnet" | ||
| }, | ||
| "repository": "https://github.com/pleaseai/claude-code-plugins", | ||
| "license": "MIT", | ||
| "keywords": ["security", "permissions", "gatekeeper", "hook"], | ||
| "hooks": "./hooks/hooks.json" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| !dist/ | ||
| !dist/** | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Gatekeeper Plugin | ||
|
|
||
| Two-layer security for Claude Code: auto-approve safe commands, AI-assisted review for the rest. | ||
|
|
||
| ## Overview | ||
|
|
||
| Gatekeeper eliminates repetitive permission dialogs for safe development commands while maintaining security against destructive operations. | ||
|
|
||
| ### How It Works | ||
|
|
||
| ``` | ||
| Bash command | ||
| │ | ||
| ▼ | ||
| Layer 1: PreToolUse (pattern matching, <5ms) | ||
| ├── ALLOW: safe patterns (npm, git, node, etc.) → skip permission dialog | ||
| ├── DENY: destructive patterns (rm -rf /, mkfs, etc.) → block immediately | ||
| └── PASSTHROUGH: unknown commands → permission dialog | ||
| │ | ||
| ▼ | ||
| Layer 2: PermissionRequest (AI agent, opus) | ||
| ├── approve → execute | ||
| └── reject → block with reason | ||
| ``` | ||
|
|
||
| ## Allowed Patterns | ||
|
|
||
| | Category | Examples | | ||
| |----------|----------| | ||
| | Package managers | `npm test`, `bun install`, `yarn add`, `pnpm run` | | ||
| | Git read | `git status`, `git log`, `git diff`, `git branch` | | ||
| | Git write | `git add`, `git commit`, `git merge`, `git pull` | | ||
| | Git push | `git push` (non-force only) | | ||
| | Build/runtime | `node`, `npx`, `tsx`, `python`, `cargo build`, `make` | | ||
| | File inspection | `ls`, `cat`, `grep`, `find`, `tree`, `wc` | | ||
| | Docker read | `docker ps`, `docker logs`, `docker images` | | ||
|
|
||
| ## Denied Patterns | ||
|
|
||
| | Pattern | Reason | | ||
| |---------|--------| | ||
| | `rm -rf /` | Filesystem root deletion | | ||
| | `rm -rf /*` | Destructive wildcard deletion from root | | ||
| | `rm -rf ~` | Home directory deletion | | ||
| | `mkfs.*` | Disk format | | ||
| | `dd if=/dev/zero of=/dev/` | Disk zeroing | | ||
|
|
||
| ## Installation | ||
|
|
||
| ```sh | ||
| claude | ||
| /plugin marketplace add pleaseai/claude-code-plugins | ||
| /plugin install gatekeeper@pleaseai | ||
| ``` | ||
|
|
||
| ## Development | ||
|
|
||
| ```bash | ||
| cd plugins/gatekeeper | ||
| bun install | ||
| bun run build | ||
| ``` | ||
|
|
||
| ### Testing | ||
|
|
||
| ```bash | ||
| # ALLOW test | ||
| echo '{"tool_name":"Bash","tool_input":{"command":"npm test"}}' | node dist/pre-tool-use.js | ||
|
|
||
| # DENY test | ||
| echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | node dist/pre-tool-use.js | ||
|
|
||
| # PASSTHROUGH test (no output) | ||
| echo '{"tool_name":"Bash","tool_input":{"command":"curl https://example.com"}}' | node dist/pre-tool-use.js | ||
| ``` | ||
|
|
||
| ## References | ||
|
|
||
| - [Claude Code Tips: PermissionRequest Hook Pattern](https://www.threads.com/@boris_cherny/post/DUMZy85EoFj) | ||
|
|
||
| ## License | ||
|
|
||
| MIT |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| // src/pre-tool-use.ts | ||
| import process from "node:process"; | ||
| var DENY_RULES = [ | ||
| { pattern: /^rm\s+-rf\s+\/(?:\s|$)/i, reason: "Filesystem root deletion blocked" }, | ||
| { pattern: /^rm\s+-rf\s+\/\*(?:\s|$)/i, reason: "Destructive wildcard deletion from root blocked" }, | ||
| { pattern: /^rm\s+-rf\s+~(?:\/|$)/i, reason: "Home directory deletion blocked" }, | ||
| { pattern: /^mkfs\./i, reason: "Disk format command blocked" }, | ||
| { | ||
| pattern: /^dd\s+if=\/dev\/zero\s+of=\/dev\//i, | ||
| reason: "Disk zeroing blocked" | ||
| } | ||
| ]; | ||
| var ALLOW_RULES = [ | ||
| { | ||
| pattern: /^(npm|yarn|pnpm|bun)\s+(test|run|install|ci|add|remove|ls|info|outdated|audit|why)\b/i, | ||
| reason: "Safe package manager command" | ||
| }, | ||
| { | ||
| pattern: /^git\s+(status|log|diff|branch|fetch|remote|tag|show|stash\s+list|rev-parse)\b/i, | ||
| reason: "Safe git read operation" | ||
| }, | ||
| { | ||
| pattern: /^git\s+(add|commit|checkout|switch|merge|rebase|stash|pull|cherry-pick)\b/i, | ||
| reason: "Safe git write operation" | ||
| }, | ||
| { | ||
| pattern: /^(node|npx|tsx|python3?|ruby|go\s+run|cargo\s+(build|run|test|check|clippy)|make|gradle|mvn)\b/i, | ||
| reason: "Safe build/runtime command" | ||
| }, | ||
| { | ||
| pattern: /^(ls|pwd|cat|head|tail|wc|file|which|type|env|echo|printf|grep|find|rg|fd|ag|tree)\b/i, | ||
| reason: "Safe file inspection command" | ||
| }, | ||
| { | ||
| pattern: /^docker\s+(ps|logs|images|inspect|version)\b/i, | ||
| reason: "Safe docker read operation" | ||
| } | ||
| ]; | ||
| function isGitPushNonForce(cmd) { | ||
| return /^git\s+push\b/i.test(cmd) && !/--force(?:-with-lease)?\b|\s-(?!-)\S*f/i.test(cmd); | ||
| } | ||
| function makeDecision(decision, reason) { | ||
| return { | ||
| hookSpecificOutput: { | ||
| hookEventName: "PreToolUse", | ||
| permissionDecision: decision, | ||
| permissionDecisionReason: reason | ||
| } | ||
| }; | ||
| } | ||
| function evaluate(input) { | ||
| if (input.tool_name !== "Bash") { | ||
| return null; | ||
| } | ||
| const cmd = input.tool_input?.command ?? ""; | ||
| if (!cmd) { | ||
| return null; | ||
| } | ||
| for (const rule of DENY_RULES) { | ||
| if (rule.pattern.test(cmd)) { | ||
| return makeDecision("deny", rule.reason); | ||
| } | ||
| } | ||
| if (/[;&|`\n]|\$\(/.test(cmd)) { | ||
| return null; | ||
| } | ||
| for (const rule of ALLOW_RULES) { | ||
| if (rule.pattern.test(cmd)) { | ||
| return makeDecision("allow", rule.reason); | ||
| } | ||
| } | ||
| if (isGitPushNonForce(cmd)) { | ||
| return makeDecision("allow", "Safe git push (non-force)"); | ||
| } | ||
| return null; | ||
| } | ||
| function readStdin() { | ||
| return new Promise((resolve, reject) => { | ||
| let data = ""; | ||
| process.stdin.setEncoding("utf8"); | ||
| process.stdin.on("data", (chunk) => { | ||
| data += chunk; | ||
| }); | ||
| process.stdin.on("end", () => resolve(data)); | ||
| process.stdin.on("error", reject); | ||
| }); | ||
| } | ||
| async function main() { | ||
| const raw = await readStdin(); | ||
| if (!raw.trim()) { | ||
| process.exit(0); | ||
| } | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(raw); | ||
| } catch (err) { | ||
| process.stderr.write(`gatekeeper: invalid JSON input: ${err instanceof Error ? err.message : String(err)} | ||
| `); | ||
| process.exit(1); | ||
| } | ||
| if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { | ||
| process.stderr.write(`gatekeeper: expected JSON object, got ${parsed === null ? "null" : typeof parsed} | ||
| `); | ||
| process.exit(1); | ||
| } | ||
| const input = parsed; | ||
| const decision = evaluate(input); | ||
| if (decision) { | ||
| process.stdout.write(JSON.stringify(decision)); | ||
| } | ||
| process.exit(0); | ||
| } | ||
| main().catch((err) => { | ||
| process.stderr.write(`gatekeeper: unexpected error: ${err instanceof Error ? err.message : String(err)} | ||
| `); | ||
| process.exit(1); | ||
| }); | ||
| export { | ||
| makeDecision, | ||
| isGitPushNonForce, | ||
| evaluate, | ||
| DENY_RULES, | ||
| ALLOW_RULES | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import antfu from '@antfu/eslint-config' | ||
|
|
||
| export default antfu({ | ||
| type: 'lib', | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| { | ||
| "description": "Gatekeeper: auto-approve safe commands + AI review", | ||
| "hooks": { | ||
| "PreToolUse": [ | ||
| { | ||
| "matcher": "Bash", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node ${CLAUDE_PLUGIN_ROOT}/dist/pre-tool-use.js", | ||
| "timeout": 5 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "PermissionRequest": [ | ||
| { | ||
| "matcher": "Bash", | ||
| "hooks": [ | ||
| { | ||
| "type": "agent", | ||
| "prompt": "You are a security analyst. This command was NOT matched by pattern-based rules (Layer 1) and needs your judgment.\n\nCommand context:\n$ARGUMENTS\n\nCheck for these attack patterns:\n1. Data destruction (rm -rf /, truncate, dd if=/dev/zero, etc.)\n2. System modification (chmod 777, chown, modifying /etc/, etc.)\n3. Network attacks (curl piping to bash, wget suspicious scripts, reverse shells)\n4. Credential exposure (cat .env, echo $API_KEY, etc.)\n5. Supply chain attacks (npm install from suspicious sources, pip install --extra-index-url)\n6. Privilege escalation (sudo without clear purpose, setuid)\n7. Command chaining hiding destructive intent — analyze ALL parts of chained commands (;, &&, ||, |, $(), backticks), not just the first\n\nScope guidance:\n- Project-scoped operations (./build, ./dist, node_modules) are generally safe\n- System-scoped operations (/etc, /usr, ~/) require careful scrutiny\n- Standard dev tools (docker run, curl localhost, ssh) are generally safe unless combined with attack patterns above\n\nYou have access to Read, Grep, Glob tools. Use them if:\n- The command references a script file (verify its contents)\n- The command uses variables that might be dangerous\n- You need to check if a path exists and what it contains\n\nAfter your analysis, respond with ONLY one of:\n{\"ok\": true}\n{\"ok\": false, \"reason\": \"Brief explanation of the specific risk\"}", | ||
| "model": "opus", | ||
| "timeout": 30 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "name": "gatekeeper", | ||
| "type": "module", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "scripts": { | ||
| "build": "bun build src/pre-tool-use.ts --outdir dist --target node", | ||
| "lint": "eslint . --cache", | ||
| "lint:fix": "eslint . --fix --cache", | ||
| "test": "bun test" | ||
| }, | ||
| "devDependencies": { | ||
| "@antfu/eslint-config": "^7.3.0", | ||
| "@anthropic-ai/claude-agent-sdk": "^0.2.37", | ||
| "eslint": "^10.0.0", | ||
| "typescript": "^5.7.0" | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.