Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@
"name": "web-design",
"description": "Review UI code for Web Interface Guidelines compliance",
"source": "./plugins/web-design"
},
{
"name": "gatekeeper",
"description": "Auto-approve safe commands, AI-assisted review for the rest",
"source": "./plugins/gatekeeper"
}
]
}
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"apps/web": "1.2.0",
"packages/eslint-config": "0.0.0",
"packages/typescript-config": "0.0.0",
"packages/vitest-config": "0.0.0"
"packages/vitest-config": "0.0.0",
"plugins/gatekeeper": "1.0.0"
}
468 changes: 468 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@
"url": "https://github.com/pleaseai/claude-code-plugins/issues"
},
"homepage": "https://github.com/pleaseai/claude-code-plugins#readme",
"packageManager": "bun@1.2.22",
"packageManager": "bun@1.3.7",
"engines": {
"node": "22.x"
},
"workspaces": [
"packages/*",
"apps/*"
"apps/*",
"plugins/*"
]
}
13 changes: 13 additions & 0 deletions plugins/gatekeeper/.claude-plugin/plugin.json
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"
}
2 changes: 2 additions & 0 deletions plugins/gatekeeper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
!dist/
Comment thread
amondnet marked this conversation as resolved.
!dist/**
83 changes: 83 additions & 0 deletions plugins/gatekeeper/README.md
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
50 changes: 50 additions & 0 deletions plugins/gatekeeper/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 124 additions & 0 deletions plugins/gatekeeper/dist/pre-tool-use.js
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
};
5 changes: 5 additions & 0 deletions plugins/gatekeeper/eslint.config.mjs
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',
})
30 changes: 30 additions & 0 deletions plugins/gatekeeper/hooks/hooks.json
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
}
]
}
]
}
}
18 changes: 18 additions & 0 deletions plugins/gatekeeper/package.json
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"
}
}
Loading