Skip to content
Open
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
28 changes: 28 additions & 0 deletions examples/README.md
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
47 changes: 47 additions & 0 deletions examples/block_destructive.py
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):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude --force-with-lease from the force-push deny regex

This pattern treats --force-with-lease as --force because --force\b matches before the hyphen, so lease-protected pushes are blocked too. git push -h documents --force-with-lease as 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 👍 / 👎.

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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Call PreToolUse decisions through ctx.output in example hook

create_context() returns a PreToolUseContext, and in this codebase decision methods live on PreToolUseOutput (ctx.output.allow()/deny() in src/cchooks/contexts/pre_tool_use.py), not on the context itself. Calling ctx.allow() here raises AttributeError for normal PreToolUse executions, so this example hook crashes instead of emitting a valid JSON decision.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix needed.

39 changes: 39 additions & 0 deletions examples/block_secrets.py
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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use ctx.output decision API in block_secrets example

Like the destructive-command example, this script calls ctx.allow()/ctx.block() on PreToolUseContext, but those methods are not defined on the context type in src/cchooks/contexts/pre_tool_use.py; only ctx.output.allow()/deny() are available. As a result, the example fails with AttributeError and never returns the hook response Claude expects.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix needed.