-
Notifications
You must be signed in to change notification settings - Fork 9
feat: add safety hook examples #6
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Examples | ||
|
|
||
| Ready-to-use hooks built with cchooks. | ||
|
|
||
| ## Safety Hooks | ||
|
|
||
| | Hook | What It Blocks | | ||
| |------|---------------| | ||
| | [block_destructive.py](block_destructive.py) | rm -rf /, git reset --hard, force push, push to main | | ||
| | [block_secrets.py](block_secrets.py) | git add .env, credential files | | ||
|
|
||
| ## Usage | ||
|
|
||
| 1. Copy the hook to your hooks directory | ||
| 2. Add to `~/.claude/settings.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [{ | ||
| "matcher": "Bash", | ||
| "hooks": [{"type": "command", "command": "python3 /path/to/block_destructive.py"}] | ||
| }] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 3. Restart Claude Code |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #!/usr/bin/env python3 | ||
| """Block destructive commands (rm -rf, git reset --hard, git clean). | ||
|
|
||
| Usage in settings.json: | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [{ | ||
| "matcher": "Bash", | ||
| "hooks": [{"type": "command", "command": "python3 /path/to/block_destructive.py"}] | ||
| }] | ||
| } | ||
| } | ||
| """ | ||
|
|
||
| import re | ||
| from cchooks import create_context, PreToolUseContext | ||
|
|
||
| ctx = create_context() | ||
|
|
||
| if not isinstance(ctx, PreToolUseContext): | ||
| ctx.allow() | ||
|
|
||
| command = ctx.tool_input.get("command", "") | ||
| if not command: | ||
| ctx.allow() | ||
|
|
||
| # Block rm -rf on sensitive paths | ||
| if re.search(r'rm\s+(-[rf]+\s+)*(\/|~|\.\.\/)', command): | ||
| ctx.block("rm on sensitive path detected. Target specific directories instead.") | ||
|
|
||
| # Block git reset --hard | ||
| if re.search(r'(^|;|&&)\s*git\s+reset\s+--hard', command): | ||
| ctx.block("git reset --hard discards all uncommitted changes. Use git stash first.") | ||
|
|
||
| # Block git clean -fd | ||
| if re.search(r'(^|;|&&)\s*git\s+clean\s+-[a-z]*[fd]', command): | ||
| ctx.block("git clean removes untracked files permanently. Use git clean -n to preview.") | ||
|
|
||
| # Block force push | ||
| if re.search(r'git\s+push\s+.*(-f\b|--force\b)', command): | ||
| ctx.block("Force push destroys remote history. Use --force-with-lease for safer option.") | ||
|
|
||
| # Block push to main/master | ||
| if re.search(r'git\s+push\s+.*\b(main|master)\b', command): | ||
| ctx.block("Push to protected branch. Push to a feature branch and create a PR.") | ||
|
|
||
| ctx.allow() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix needed. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| #!/usr/bin/env python3 | ||
| """Block staging secret files (git add .env, credentials). | ||
|
|
||
| Usage in settings.json: | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [{ | ||
| "matcher": "Bash", | ||
| "hooks": [{"type": "command", "command": "python3 /path/to/block_secrets.py"}] | ||
| }] | ||
| } | ||
| } | ||
| """ | ||
|
|
||
| import re | ||
| from cchooks import create_context, PreToolUseContext | ||
|
|
||
| ctx = create_context() | ||
|
|
||
| if not isinstance(ctx, PreToolUseContext): | ||
| ctx.allow() | ||
|
|
||
| command = ctx.tool_input.get("command", "") | ||
| if not command: | ||
| ctx.allow() | ||
|
|
||
| # Only check git add commands | ||
| if not re.search(r'^\s*git\s+add', command): | ||
| ctx.allow() | ||
|
|
||
| # Block .env files | ||
| if re.search(r'git\s+add\s+.*\.env(\s|$|\.)', command, re.IGNORECASE): | ||
| ctx.block(".env file contains secrets. Add to .gitignore instead.") | ||
|
|
||
| # Block credential/key files | ||
| if re.search(r'git\s+add\s+.*(credentials|\.pem|\.key|\.p12|id_rsa|id_ed25519)', command, re.IGNORECASE): | ||
| ctx.block("Credential/key file should never be committed. Add to .gitignore.") | ||
|
|
||
| ctx.allow() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Like the destructive-command example, this script calls Useful? React with 👍 / 👎.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix needed. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This pattern treats
--force-with-leaseas--forcebecause--force\bmatches before the hyphen, so lease-protected pushes are blocked too.git push -hdocuments--force-with-leaseas a separate, safer option, and your deny message explicitly tells users to use it; with the current regex, that recommendation cannot work.Useful? React with 👍 / 👎.