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
32 changes: 25 additions & 7 deletions .claude/commands/new-hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,16 @@ Hooks can return structured responses:
suppressOutput?: boolean
}

// Other hooks can return:
// Stop/SubagentStop can return:
{
decision?: 'block',
reason?: string,
continue?: boolean,
stopReason?: string,
suppressOutput?: boolean
}

// Notification can return:
{
continue?: boolean,
stopReason?: string,
Expand Down Expand Up @@ -139,22 +148,31 @@ export { blockDangerousCommands } from './hooks/blockDangerousCommands';
- Each hook type has its own specific input type
- Share common logic between related hooks when appropriate

### 5. Update Documentation
### 5. Update Exports

After creating a new hook, update the `src/index.ts` file to export it:

```typescript
// Add your export to src/index.ts
export { myHookName } from './hooks/myHookName';
```

### 6. Update Documentation

After creating a new hook, update the README.md file to document it:
After creating and exporting the hook, update the README.md file to document it:

1. Add the new hook to the "Predefined Hook Utilities" section
2. Include usage examples showing how to import and use the hook
3. Document any configuration options
4. Show example output if the hook produces logs or other artifacts

### 6. Using the Hook
### 7. Using the Hook

Users can then import and use the hook:

```typescript
// .claude/hooks/hooks.ts
import { defineHooks, blockDangerousCommands } from 'define-claude-code-hooks';
import { defineHooks, blockDangerousCommands } from '@timoaus/define-claude-code-hooks';

export default defineHooks({
PreToolUse: [
Expand All @@ -164,7 +182,7 @@ export default defineHooks({
});
```

### 7. Common Patterns and Best Practices
### 8. Common Patterns and Best Practices

**File I/O in hooks:**
- Use `process.cwd()` to get the current working directory for file paths
Expand Down Expand Up @@ -198,7 +216,7 @@ export default defineHooks({
- Clean up test artifacts after running
- Test edge cases like file size limits and error conditions

### 8. Advanced Hook Patterns
### 9. Advanced Hook Patterns

**Tool hooks with matchers:**
- Tool hooks (PreToolUse, PostToolUse) require both `matcher` and `handler`
Expand Down
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ The CLI will automatically detect which hook files exist and update the correspo

The library includes several predefined hook utilities for common logging scenarios:

| Hook Function | Options |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`logPreToolUseEvents`**<br/>Logs tool uses before execution | • Optional first param: `matcher` (regex pattern, defaults to '.\*' for all tools)<br/>• `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.tool-use.json')<br/>• `includeToolInput` (default: true) |
| **`logPostToolUseEvents`**<br/>Logs tool uses after execution | • Optional first param: `matcher` (regex pattern, defaults to '.\*' for all tools)<br/>• `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.tool-use.json')<br/>• `includeToolInput` (default: true)<br/>• `includeToolResponse` (default: true) |
| **`logStopEvents`**<br/>Logs main agent stop events | • `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.stop.json') |
| **`logSubagentStopEvents`**<br/>Logs subagent stop events | • `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.stop.json') |
| **`logNotificationEvents`**<br/>Logs notification messages | • `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.notification.json') |

| Hook Function | Options |
|--------------|---------|
| **`logPreToolUseEvents`**<br/>Logs tool uses before execution | • Optional first param: `matcher` (regex pattern, defaults to '.*' for all tools)<br/>• `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.tool-use.json')<br/>• `includeToolInput` (default: true) |
| **`logPostToolUseEvents`**<br/>Logs tool uses after execution | • Optional first param: `matcher` (regex pattern, defaults to '.*' for all tools)<br/>• `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.tool-use.json')<br/>• `includeToolInput` (default: true)<br/>• `includeToolResponse` (default: true) |
| **`logStopEvents`**<br/>Logs main agent stop events | • `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.stop.json') |
| **`logSubagentStopEvents`**<br/>Logs subagent stop events | • `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.stop.json') |
| **`logNotificationEvents`**<br/>Logs notification messages | • `maxEventsStored` (default: 100)<br/>• `logFileName` (default: 'hook-log.notification.json') |
| **`blockEnvFiles`**<br/>Blocks access to .env files | No options - blocks all .env file variants except example files |

All predefined hooks:

Expand Down Expand Up @@ -333,6 +335,27 @@ export default defineHooks({
});
```

### Environment File Protection

```typescript
import {
defineHooks,
blockEnvFiles,
} from "@timoaus/define-claude-code-hooks";

export default defineHooks({
PreToolUse: [
blockEnvFiles, // Blocks access to .env files while allowing .env.example
],
});
```

The `blockEnvFiles` hook:
- Blocks reading or writing to `.env` files and variants (`.env.local`, `.env.production`, etc.)
- Allows access to example env files (`.env.example`, `.env.sample`, `.env.template`, `.env.dist`)
- Works with `Read`, `Write`, `Edit`, and `MultiEdit` tools
- Provides clear error messages when access is blocked

### Combining Multiple Hooks

```typescript
Expand All @@ -341,10 +364,12 @@ import {
logStopEvents,
logPreToolUseEvents,
logPostToolUseEvents,
blockEnvFiles,
} from "@timoaus/define-claude-code-hooks";

export default defineHooks({
PreToolUse: [
blockEnvFiles, // Security: prevent .env file access
logPreToolUseEvents({ logFileName: "hook-log.tool-use.json" }),
// Add your custom hooks here
{
Expand Down
59 changes: 59 additions & 0 deletions src/hooks/blockEnvFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { defineHook } from '../index';
import { PreToolUseInput } from '../types';

/**
* Blocks reading or writing of .env files and variants (e.g., .env.local, .env.production)
* while allowing example env files (e.g., .env.example, .env.sample)
*
* @example
* ```typescript
* import { defineHooks, blockEnvFiles } from 'define-claude-code-hooks';
*
* export default defineHooks({
* PreToolUse: [
* blockEnvFiles,
* // other hooks...
* ]
* });
* ```
*/
export const blockEnvFiles = defineHook('PreToolUse', {
matcher: 'Read|Write|Edit|MultiEdit',
handler: async (input: PreToolUseInput) => {
const toolName = input.tool_name;
let filePath: string | undefined;

// Extract file path based on tool
if (toolName === 'Read' || toolName === 'Write') {
filePath = input.tool_input.file_path;
} else if (toolName === 'Edit' || toolName === 'MultiEdit') {
filePath = input.tool_input.file_path;
}

if (!filePath) {
return; // No file path to check
}

// Normalize the file path for consistent checking
const normalizedPath = filePath.toLowerCase();

// Check if this is an example env file (allowed)
const isExampleFile = /\.env\.(example|sample|template|dist)$/i.test(filePath) ||
/\.env\..*\.(example|sample|template|dist)$/i.test(filePath);

if (isExampleFile) {
return; // Allow example env files
}

// Check if this is a .env file or variant (blocked)
const isEnvFile = /\.env$/i.test(filePath) || // .env
/\.env\.[^.]+$/i.test(filePath); // .env.local, .env.production, etc.

if (isEnvFile) {
return {
decision: 'block' as const,
reason: `Access to .env files is not allowed. File: ${filePath}. If you need to show an example, use .env.example or .env.sample instead.`
};
}
}
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
logPreToolUseEvents,
logPostToolUseEvents
} from './hooks/logToolUseEvents';
export { blockEnvFiles } from './hooks/blockEnvFiles';

/**
* Define a typed hook handler for Claude Code
Expand Down