Back to skills
SKILL.md
Clarc Hooks Authoring
ASecurityReference guide for writing, testing, and configuring clarc hooks — PreToolUse, PostToolUse, SessionStart, SessionEnd patterns with suppression and cooldown.
- 4 stars
- 0 votes
- 0 copies
- 3 views
- Added September 12, 2026
Works with
Security analysis
100/100Pro scans all 2 files and shows the line behind each finding
npx -y skills add aibot88/sec_skill_store --skill clarc-hooks-authoring --agent claude-codeAre you the author of Clarc Hooks Authoring?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/aibot88-clarc-hooks-authoring)---
name: clarc-hooks-authoring
description: Reference guide for writing, testing, and configuring clarc hooks — PreToolUse, PostToolUse, SessionStart, SessionEnd patterns with suppression and cooldown.
---
## When to Use
Use this skill when:
- Adding a new hook to clarc's hook system
- Debugging why a hook fires or doesn't fire
- Configuring per-project hook suppression
- Understanding the difference between blocking and advisory hooks
- Writing a PreToolUse hook that validates or blocks a Bash command before Claude executes it
- Creating a PostToolUse nudge that reminds the developer to run a security review after editing auth-related files
- Implementing a cooldown mechanism so an advisory nudge fires at most once per N minutes instead of on every file save
- Combining multiple checks for the same event into a single dispatch script to avoid spawning one Node.js process per check
## Hook System Overview
Hooks are Node.js scripts in `scripts/hooks/` registered in `hooks/hooks.json`.
They intercept Claude Code tool calls at specific lifecycle events.
```
hooks/hooks.json ← registration (event, matcher, command)
scripts/hooks/*.js ← implementation scripts
.clarc/hooks-config.json ← per-project suppression config
~/.clarc/hooks-config.json ← global suppression config
```
## Event Types
| Event | When | Common Uses |
|-------|------|-------------|
| `PreToolUse` | Before tool executes | Validation, blocking, warnings |
| `PostToolUse` | After tool completes | Formatting, nudges, logging |
| `SessionStart` | New session begins | Context loading, project detection |
| `SessionEnd` | Session closes | State saving, weekly tasks |
| `PreCompact` | Before context compaction | Save important state |
| `Stop` | After each response | Final checks |
| `Notification` | Claude needs attention | Log + respond to notifications |
## Hook Registration Format
```json
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/my-hook.js\"",
"async": true,
"timeout": 5
}
],
"description": "What this hook does"
}
```
- `matcher`: Tool name or `*` for all. Pipe-separated for multiple: `"Edit|Write"`
- `async: true`: Hook runs in background — response is not delayed (advisory hooks should be async)
- `timeout`: Seconds before hook is killed (default varies; set explicitly for async hooks)
- `${CLAUDE_PLUGIN_ROOT}`: Resolved to clarc root directory at runtime
## Exit Codes
| Exit Code | Meaning |
|-----------|---------|
| `0` | Success — allow tool execution |
| `2` | Block — tool execution is prevented (PreToolUse only) |
| Other | Error — treated as exit 0 (non-blocking) |
## Script Pattern: PreToolUse
```javascript
#!/usr/bin/env node
import { logHook } from './hook-logger.js';
const MAX_STDIN = 1024 * 1024;
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => { data += chunk; });
process.stdin.on('end', () => {
const start = Date.now();
try {
const input = JSON.parse(data);
const cmd = input.tool_input?.command || ''; // Bash
const filePath = input.tool_input?.file_path || ''; // Edit/Write
if (shouldBlock(cmd)) {
console.error('[my-hook] BLOCKED: reason');
logHook('my-hook', 'Bash', cmd.slice(0, 60), 2, Date.now() - start);
process.exit(2); // ← blocks the tool
}
} catch { /* pass through on parse error */ }
process.stdout.write(data); // pass-through required for PreToolUse
process.exit(0);
});
```
## Script Pattern: PostToolUse
```javascript
#!/usr/bin/env node
process.stdin.setEncoding('utf8');
let data = '';
process.stdin.on('data', chunk => { data += chunk; });
process.stdin.on('end', () => {
try {
const input = JSON.parse(data);
const filePath = input.tool_input?.file_path || '';
const output = input.tool_response?.output || ''; // tool's return value
const toolName = input.tool_name || ''; // "Edit" | "Write" | "Bash" etc.
if (shouldNudge(filePath)) {
console.error('[my-hook] Advisory: do something');
}
} catch { /* ignore */ }
process.exit(0); // PostToolUse: always exit 0
});
```
## Suppression Config
Users can disable individual hooks per-project or globally:
```json
// .clarc/hooks-config.json (project-local)
// ~/.clarc/hooks-config.json (global)
{
"disabled": [
"code-review-nudge",
"tdd-sequence-guard"
],
"code_review_cooldown_minutes": 10
}
```
Check suppression in your hook:
```javascript
import fs from 'fs';
import os from 'os';
import path from 'path';
function isDisabled(hookId) {
for (const p of [
path.join(process.cwd(), '.clarc', 'hooks-config.json'),
path.join(os.homedir(), '.clarc', 'hooks-config.json'),
]) {
try {
const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
if (cfg.disabled?.includes(hookId)) return true;
} catch { /* not found */ }
}
return false;
}
```
## Performance Constraints
| Hook Type | Max Duration | Rule |
|-----------|-------------|------|
| PreToolUse (blocking) | < 500ms | User waits; must be fast |
| PostToolUse (sync) | < 500ms | Delays Claude response |
| PostToolUse (async) | < 2s recommended | Background; timeout set in hooks.json |
| SessionStart | < 2s | Acceptable startup cost |
- No network calls in hooks (offline-first, no latency)
- No spawning child processes unless absolutely needed (costs ~50ms on macOS)
- Use `async: true` for all advisory (non-blocking) PostToolUse hooks
## Dispatch Pattern
When multiple checks share the same event+matcher, combine them into one dispatch script:
```javascript
// pre-bash-dispatch.js — one process handles N checks
const checks = [devServerCheck, tmuxReminder, gitPushReminder, secretGuard];
for (const check of checks) {
const result = check(cmd);
if (result.block) { process.exit(2); }
}
```
This avoids spawning N Node.js processes per tool call.
## Cooldown Pattern
For nudges that would spam on every save:
```javascript
const COOLDOWN_PATH = path.join(os.homedir(), '.clarc', 'nudge-cooldown.json');
function isCoolingDown(hookId, minutes = 5) {
try {
const c = JSON.parse(fs.readFileSync(COOLDOWN_PATH, 'utf8'));
return c[hookId] && (Date.now() - c[hookId]) < minutes * 60 * 1000;
} catch { return false; }
}
function setCooldown(hookId) {
let c = {};
try { c = JSON.parse(fs.readFileSync(COOLDOWN_PATH, 'utf8')); } catch {}
c[hookId] = Date.now();
try { fs.writeFileSync(COOLDOWN_PATH, JSON.stringify(c)); } catch {}
}
```
## Anti-patterns
```javascript
// WRONG: network call in hook
const res = await fetch('https://api.example.com/check'); // adds latency
// WRONG: missing pass-through in PreToolUse
process.exit(0); // without process.stdout.write(data) — passes empty input to tool
// WRONG: no error handling
const input = JSON.parse(data); // throws if stdin is empty or malformed
// CORRECT: always wrap in try/catch, always write data for PreToolUse
try {
const input = JSON.parse(data);
// ...
} catch { /* pass through */ }
process.stdout.write(data); // required for PreToolUse
process.exit(0);
```
## Testing a Hook
```bash
# Simulate PreToolUse (Bash)
echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' | \
node scripts/hooks/pre-bash-dispatch.js
# Simulate PostToolUse (Edit)
echo '{"tool_name":"Edit","tool_input":{"file_path":"/project/src/auth/service.ts"}}' | \
node scripts/hooks/post-edit-workflow-nudge.js
# View hook log
tail -20 ~/.claude/hooks.log | node -e "process.stdin.on('data',d=>d.toString().trim().split('\n').forEach(l=>{try{console.log(JSON.parse(l))}catch{}}))"
```
Files in this skill
- SKILL.md
- source.json
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.