@secemp/elwood 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/examples/01-basic-query.js +38 -0
  4. package/examples/02-bug-fixer.js +57 -0
  5. package/examples/03-custom-system-prompt.js +41 -0
  6. package/examples/04-read-only-agent.js +38 -0
  7. package/examples/05-find-todos.js +37 -0
  8. package/examples/06-session-resume.js +70 -0
  9. package/examples/07-hooks-pretooluse.js +74 -0
  10. package/examples/08-hooks-posttooluse-audit.js +66 -0
  11. package/examples/09-hooks-block-etc.js +68 -0
  12. package/examples/10-hooks-redirect-sandbox.js +70 -0
  13. package/examples/11-subagents.js +54 -0
  14. package/examples/12-mcp-stdio.js +48 -0
  15. package/examples/13-mcp-http.js +54 -0
  16. package/examples/14-custom-tool.js +84 -0
  17. package/examples/15-custom-tool-unit-converter.js +132 -0
  18. package/examples/16-mcp-github.js +71 -0
  19. package/examples/17-session-store-postgres.js +78 -0
  20. package/examples/18-session-store-redis.js +65 -0
  21. package/examples/19-session-store-s3.js +67 -0
  22. package/examples/20-session-list.js +72 -0
  23. package/examples/21-hooks-notification-slack.js +78 -0
  24. package/examples/22-hooks-webhook-posttooluse.js +78 -0
  25. package/examples/23-hooks-subagent-tracker.js +59 -0
  26. package/examples/24-v2-session-api.js +62 -0
  27. package/examples/README.md +95 -0
  28. package/examples/basic.js +240 -0
  29. package/examples/smoke-test.js +296 -0
  30. package/package.json +52 -0
  31. package/src/ast-tools.js +182 -0
  32. package/src/index.js +70 -0
  33. package/src/instrumenter.js +2921 -0
  34. package/src/loader.js +306 -0
  35. package/src/locate.js +296 -0
  36. package/src/query.js +2168 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 secemp9
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # elwood
2
+
3
+ Programmatically import and instrument the locally installed Claude Code CLI via Babel AST. Drop-in replacement for `@anthropic-ai/claude-agent-sdk` that runs **in-process** -- zero subprocess spawning.
4
+
5
+ ## How it works
6
+
7
+ Instead of spawning `claude -p` as a subprocess (like the official SDK), elwood:
8
+
9
+ 1. Locates the installed Claude Code CLI (`cli.js`)
10
+ 2. Parses the 13MB bundle with Babel AST
11
+ 3. Fingerprints internal functions via evidence-based pattern matching
12
+ 4. Injects instrumentation hooks
13
+ 5. Calls `agentLoop()` directly in-process
14
+
15
+ This gives you the same API as `@anthropic-ai/claude-agent-sdk` with lower latency and direct access to internals.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install elwood
21
+ ```
22
+
23
+ Requires Claude Code to be installed:
24
+ ```bash
25
+ npm install -g @anthropic-ai/claude-code
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```js
31
+ import { query } from 'elwood';
32
+
33
+ for await (const message of query({
34
+ prompt: 'What files are in this directory?',
35
+ options: { maxTurns: 3 },
36
+ })) {
37
+ if (message.type === 'result') {
38
+ console.log(message.result);
39
+ }
40
+ }
41
+ ```
42
+
43
+ No API key needed if logged in via `claude login` (Pro/Max subscription).
44
+
45
+ ## API
46
+
47
+ ### query({ prompt, options })
48
+
49
+ Returns an async generator yielding messages. Options include:
50
+
51
+ - `model` -- model to use
52
+ - `systemPrompt` -- string or `{type: 'preset', append: '...'}`
53
+ - `maxTurns` -- maximum conversation turns
54
+ - `allowedTools` -- auto-approve these tools
55
+ - `hooks` -- `{PreToolUse: [...], PostToolUse: [...], ...}`
56
+ - `mcpServers` -- MCP server configs
57
+ - `permissionMode` -- `'default' | 'acceptEdits' | 'bypassPermissions'`
58
+ - `continue` / `resume` / `resumeSessionAt` / `forkSession` -- session management
59
+ - `env` / `stderr` -- environment and output control
60
+ - ...and all other official SDK options
61
+
62
+ ### Query control methods
63
+
64
+ ```js
65
+ const conversation = query({ prompt: '...' });
66
+ conversation.interrupt(); // soft interrupt
67
+ conversation.setModel('...'); // change model mid-conversation
68
+ conversation.setPermissionMode('acceptEdits');
69
+ await conversation.supportedModels();
70
+ await conversation.supportedCommands();
71
+ conversation.close(); // cleanup
72
+ ```
73
+
74
+ ### tool(name, description, schema, handler)
75
+
76
+ ```js
77
+ import { tool, createSdkMcpServer, query } from 'elwood';
78
+
79
+ const weather = tool('get_weather', 'Get weather',
80
+ { type: 'object', properties: { city: { type: 'string' } } },
81
+ ({ city }) => `72F in ${city}`
82
+ );
83
+
84
+ const server = await createSdkMcpServer({ tools: [weather] });
85
+
86
+ for await (const msg of query({
87
+ prompt: 'Weather in NYC?',
88
+ options: { mcpServers: [server] },
89
+ })) { /* ... */ }
90
+ ```
91
+
92
+ ### Session utilities
93
+
94
+ ```js
95
+ import { listSessions, getSessionMessages, getSessionInfo } from 'elwood';
96
+
97
+ const sessions = await listSessions();
98
+ const messages = await getSessionMessages(sessions[0].id);
99
+ ```
100
+
101
+ ### V2 Session API
102
+
103
+ ```js
104
+ import { unstable_v2_createSession, unstable_v2_prompt } from 'elwood';
105
+
106
+ const result = await unstable_v2_prompt('Explain recursion');
107
+ ```
108
+
109
+ ### Low-level AST access
110
+
111
+ ```js
112
+ import { locate, load, instrument, parseBundle } from 'elwood';
113
+
114
+ const { cliPath, version } = await locate();
115
+ const ast = parseBundle(cliPath);
116
+ ```
117
+
118
+ ## Auth
119
+
120
+ Works with all Claude Code auth methods:
121
+ - Claude Pro/Max subscription (`claude login`)
122
+ - `ANTHROPIC_API_KEY` environment variable
123
+ - OAuth tokens
124
+ - Bedrock/Vertex/Foundry
125
+
126
+ ## Examples
127
+
128
+ See the [examples/](./examples/) directory for 24 working examples covering queries, hooks, MCP, custom tools, sessions, and more.
129
+
130
+ ## License
131
+
132
+ MIT
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 01-basic-query.js
3
+ *
4
+ * Equivalent of the official @anthropic-ai/claude-agent-sdk "basic query" example.
5
+ *
6
+ * Official version:
7
+ * import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ * for await (const message of query({
9
+ * prompt: "What files are in this directory?",
10
+ * options: { allowedTools: ["Bash", "Glob"] }
11
+ * })) {
12
+ * if ("result" in message) console.log(message.result);
13
+ * }
14
+ *
15
+ * This elwood version uses the in-process query() function which runs
16
+ * cli.js via Babel AST instrumentation instead of spawning a subprocess.
17
+ *
18
+ * Prerequisites:
19
+ * - ANTHROPIC_API_KEY set in environment
20
+ * - Claude Code CLI installed (`npm install -g @anthropic-ai/claude-code`)
21
+ *
22
+ * Run:
23
+ * node examples/01-basic-query.js
24
+ */
25
+
26
+ import { query } from '../src/index.js';
27
+
28
+ for await (const message of query({
29
+ prompt: 'What files are in this directory?',
30
+ options: {
31
+ allowedTools: ['Bash', 'Glob'],
32
+ maxTurns: 3,
33
+ },
34
+ })) {
35
+ if ('result' in message) {
36
+ console.log(message.result);
37
+ }
38
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 02-bug-fixer.js
3
+ *
4
+ * Equivalent of the official quickstart "bug fixer" agent example.
5
+ *
6
+ * Official version:
7
+ * import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ * for await (const message of query({
9
+ * prompt: "Review utils.py for bugs that would cause crashes. Fix any issues you find.",
10
+ * options: {
11
+ * allowedTools: ["Read", "Edit", "Glob"],
12
+ * permissionMode: "acceptEdits"
13
+ * }
14
+ * })) {
15
+ * if (message.type === "assistant" && message.message?.content) {
16
+ * for (const block of message.message.content) {
17
+ * if ("text" in block) console.log(block.text);
18
+ * else if ("name" in block) console.log(`Tool: ${block.name}`);
19
+ * }
20
+ * } else if (message.type === "result") {
21
+ * console.log(`Done: ${message.subtype}`);
22
+ * }
23
+ * }
24
+ *
25
+ * This elwood version runs the same agent loop in-process via Babel AST.
26
+ *
27
+ * Prerequisites:
28
+ * - ANTHROPIC_API_KEY set in environment
29
+ * - Claude Code CLI installed
30
+ *
31
+ * Run:
32
+ * node examples/02-bug-fixer.js
33
+ */
34
+
35
+ import { query } from '../src/index.js';
36
+
37
+ // Agentic loop: streams messages as Claude works
38
+ for await (const message of query({
39
+ prompt: 'Review utils.py for bugs that would cause crashes. Fix any issues you find.',
40
+ options: {
41
+ allowedTools: ['Read', 'Edit', 'Glob'], // Tools Claude can use
42
+ permissionMode: 'acceptEdits', // Auto-approve file edits
43
+ },
44
+ })) {
45
+ // Print human-readable output
46
+ if (message.type === 'assistant' && message.message?.content) {
47
+ for (const block of message.message.content) {
48
+ if ('text' in block) {
49
+ console.log(block.text); // Claude's reasoning
50
+ } else if ('name' in block) {
51
+ console.log(`Tool: ${block.name}`); // Tool being called
52
+ }
53
+ }
54
+ } else if (message.type === 'result') {
55
+ console.log(`Done: ${message.subtype}`); // Final result
56
+ }
57
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 03-custom-system-prompt.js
3
+ *
4
+ * Equivalent of the official "custom system prompt" example from the quickstart.
5
+ *
6
+ * Official version:
7
+ * options: {
8
+ * allowedTools: ["Read", "Edit", "Glob"],
9
+ * permissionMode: "acceptEdits",
10
+ * systemPrompt: "You are a senior Python developer. Always follow PEP 8 style guidelines."
11
+ * }
12
+ *
13
+ * This elwood version uses the in-process query() with a custom system prompt.
14
+ *
15
+ * Prerequisites:
16
+ * - ANTHROPIC_API_KEY set in environment
17
+ * - Claude Code CLI installed
18
+ *
19
+ * Run:
20
+ * node examples/03-custom-system-prompt.js
21
+ */
22
+
23
+ import { query } from '../src/index.js';
24
+
25
+ for await (const message of query({
26
+ prompt: 'Add type hints to all functions in utils.py',
27
+ options: {
28
+ allowedTools: ['Read', 'Edit', 'Glob'],
29
+ permissionMode: 'acceptEdits',
30
+ systemPrompt: 'You are a senior Python developer. Always follow PEP 8 style guidelines.',
31
+ maxTurns: 5,
32
+ },
33
+ })) {
34
+ if (message.type === 'assistant' && message.message?.content) {
35
+ for (const block of message.message.content) {
36
+ if ('text' in block) console.log(block.text);
37
+ }
38
+ } else if (message.type === 'result') {
39
+ console.log(`Done: ${message.subtype}`);
40
+ }
41
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 04-read-only-agent.js
3
+ *
4
+ * Equivalent of the official "permissions / read-only agent" example.
5
+ *
6
+ * Official version:
7
+ * import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ * for await (const message of query({
9
+ * prompt: "Review this code for best practices",
10
+ * options: { allowedTools: ["Read", "Glob", "Grep"] }
11
+ * })) {
12
+ * if ("result" in message) console.log(message.result);
13
+ * }
14
+ *
15
+ * This example creates a read-only agent that can analyze but never modify code.
16
+ * Only Read, Glob, and Grep are allowed -- no Write, Edit, or Bash.
17
+ *
18
+ * Prerequisites:
19
+ * - ANTHROPIC_API_KEY set in environment
20
+ * - Claude Code CLI installed
21
+ *
22
+ * Run:
23
+ * node examples/04-read-only-agent.js
24
+ */
25
+
26
+ import { query } from '../src/index.js';
27
+
28
+ for await (const message of query({
29
+ prompt: 'Review this code for best practices',
30
+ options: {
31
+ allowedTools: ['Read', 'Glob', 'Grep'],
32
+ maxTurns: 5,
33
+ },
34
+ })) {
35
+ if ('result' in message) {
36
+ console.log(message.result);
37
+ }
38
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 05-find-todos.js
3
+ *
4
+ * Equivalent of the official "find TODO comments" example.
5
+ *
6
+ * Official version:
7
+ * import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ * for await (const message of query({
9
+ * prompt: "Find all TODO comments and create a summary",
10
+ * options: { allowedTools: ["Read", "Glob", "Grep"] }
11
+ * })) {
12
+ * if ("result" in message) console.log(message.result);
13
+ * }
14
+ *
15
+ * This elwood version searches for TODO comments using the in-process agent.
16
+ *
17
+ * Prerequisites:
18
+ * - ANTHROPIC_API_KEY set in environment
19
+ * - Claude Code CLI installed
20
+ *
21
+ * Run:
22
+ * node examples/05-find-todos.js
23
+ */
24
+
25
+ import { query } from '../src/index.js';
26
+
27
+ for await (const message of query({
28
+ prompt: 'Find all TODO comments and create a summary',
29
+ options: {
30
+ allowedTools: ['Read', 'Glob', 'Grep'],
31
+ maxTurns: 10,
32
+ },
33
+ })) {
34
+ if ('result' in message) {
35
+ console.log(message.result);
36
+ }
37
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 06-session-resume.js
3
+ *
4
+ * Equivalent of the official "sessions" example showing session capture and resume.
5
+ *
6
+ * Official version:
7
+ * import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ *
9
+ * let sessionId;
10
+ * // First query: capture the session ID
11
+ * for await (const message of query({
12
+ * prompt: "Read the authentication module",
13
+ * options: { allowedTools: ["Read", "Glob"] }
14
+ * })) {
15
+ * if (message.type === "system" && message.subtype === "init") {
16
+ * sessionId = message.session_id;
17
+ * }
18
+ * }
19
+ *
20
+ * // Resume with full context from the first query
21
+ * for await (const message of query({
22
+ * prompt: "Now find all places that call it",
23
+ * options: { resume: sessionId }
24
+ * })) {
25
+ * if ("result" in message) console.log(message.result);
26
+ * }
27
+ *
28
+ * This elwood version captures the session ID from the init message and
29
+ * resumes the session in a second query, maintaining full conversation context.
30
+ *
31
+ * Prerequisites:
32
+ * - ANTHROPIC_API_KEY set in environment
33
+ * - Claude Code CLI installed
34
+ *
35
+ * Run:
36
+ * node examples/06-session-resume.js
37
+ */
38
+
39
+ import { query } from '../src/index.js';
40
+
41
+ let sessionId;
42
+
43
+ // First query: capture the session ID
44
+ console.log('--- First query: reading the authentication module ---');
45
+ for await (const message of query({
46
+ prompt: 'Read the authentication module',
47
+ options: {
48
+ allowedTools: ['Read', 'Glob'],
49
+ maxTurns: 3,
50
+ },
51
+ })) {
52
+ if (message.type === 'system' && message.subtype === 'init') {
53
+ sessionId = message.session_id;
54
+ console.log(`Session ID captured: ${sessionId}`);
55
+ }
56
+ }
57
+
58
+ // Resume with full context from the first query
59
+ console.log('\n--- Second query: resuming to find callers ---');
60
+ for await (const message of query({
61
+ prompt: 'Now find all places that call it', // "it" = auth module from context
62
+ options: {
63
+ resume: sessionId,
64
+ maxTurns: 5,
65
+ },
66
+ })) {
67
+ if ('result' in message) {
68
+ console.log(message.result);
69
+ }
70
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 07-hooks-pretooluse.js
3
+ *
4
+ * Equivalent of the official "hooks" example showing PreToolUse to protect .env files.
5
+ *
6
+ * Official version:
7
+ * import { query, HookCallback } from "@anthropic-ai/claude-agent-sdk";
8
+ *
9
+ * const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
10
+ * const preInput = input as PreToolUseHookInput;
11
+ * const toolInput = preInput.tool_input as Record<string, unknown>;
12
+ * const filePath = toolInput?.file_path as string;
13
+ * const fileName = filePath?.split("/").pop();
14
+ *
15
+ * if (fileName === ".env") {
16
+ * return {
17
+ * hookSpecificOutput: {
18
+ * hookEventName: preInput.hook_event_name,
19
+ * permissionDecision: "deny",
20
+ * permissionDecisionReason: "Cannot modify .env files"
21
+ * }
22
+ * };
23
+ * }
24
+ * return {};
25
+ * };
26
+ *
27
+ * This elwood version uses the same hooks API (callback-based).
28
+ *
29
+ * Prerequisites:
30
+ * - ANTHROPIC_API_KEY set in environment
31
+ * - Claude Code CLI installed
32
+ *
33
+ * Run:
34
+ * node examples/07-hooks-pretooluse.js
35
+ */
36
+
37
+ import { query } from '../src/index.js';
38
+
39
+ // Define a hook callback that blocks writes to .env files
40
+ const protectEnvFiles = async (input, toolUseID, { signal }) => {
41
+ const toolInput = input.tool_input;
42
+ const filePath = typeof toolInput === 'object' ? toolInput?.file_path : undefined;
43
+ const fileName = typeof filePath === 'string' ? filePath.split('/').pop() : '';
44
+
45
+ // Block the operation if targeting a .env file
46
+ if (fileName === '.env') {
47
+ return {
48
+ hookSpecificOutput: {
49
+ hookEventName: input.hook_event_name,
50
+ permissionDecision: 'deny',
51
+ permissionDecisionReason: 'Cannot modify .env files',
52
+ },
53
+ };
54
+ }
55
+
56
+ // Return empty object to allow the operation
57
+ return {};
58
+ };
59
+
60
+ for await (const message of query({
61
+ prompt: 'Update the database configuration',
62
+ options: {
63
+ hooks: {
64
+ // Register the hook for PreToolUse events
65
+ // The matcher filters to only Write and Edit tool calls
66
+ PreToolUse: [{ matcher: 'Write|Edit', hooks: [protectEnvFiles] }],
67
+ },
68
+ maxTurns: 3,
69
+ },
70
+ })) {
71
+ if (message.type === 'assistant' || message.type === 'result') {
72
+ console.log(message);
73
+ }
74
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 08-hooks-posttooluse-audit.js
3
+ *
4
+ * Equivalent of the official "PostToolUse audit log" hooks example.
5
+ *
6
+ * Official version:
7
+ * import { query, HookCallback } from "@anthropic-ai/claude-agent-sdk";
8
+ * import { appendFile } from "fs/promises";
9
+ *
10
+ * const logFileChange: HookCallback = async (input) => {
11
+ * const filePath = (input as any).tool_input?.file_path ?? "unknown";
12
+ * await appendFile("./audit.log",
13
+ * `${new Date().toISOString()}: modified ${filePath}\n`);
14
+ * return {};
15
+ * };
16
+ *
17
+ * for await (const message of query({
18
+ * prompt: "Refactor utils.py to improve readability",
19
+ * options: {
20
+ * permissionMode: "acceptEdits",
21
+ * hooks: {
22
+ * PostToolUse: [{ matcher: "Edit|Write", hooks: [logFileChange] }]
23
+ * }
24
+ * }
25
+ * })) {
26
+ * if ("result" in message) console.log(message.result);
27
+ * }
28
+ *
29
+ * This elwood version logs all file changes to an audit.log file
30
+ * using a PostToolUse hook.
31
+ *
32
+ * Prerequisites:
33
+ * - ANTHROPIC_API_KEY set in environment
34
+ * - Claude Code CLI installed
35
+ *
36
+ * Run:
37
+ * node examples/08-hooks-posttooluse-audit.js
38
+ */
39
+
40
+ import { query } from '../src/index.js';
41
+ import { appendFile } from 'node:fs/promises';
42
+
43
+ // Define a hook callback that logs file modifications
44
+ const logFileChange = async (input, _toolUseID, _context) => {
45
+ const filePath = input?.tool_input?.file_path ?? 'unknown';
46
+ await appendFile(
47
+ './audit.log',
48
+ `${new Date().toISOString()}: modified ${filePath}\n`
49
+ );
50
+ return {};
51
+ };
52
+
53
+ for await (const message of query({
54
+ prompt: 'Refactor utils.py to improve readability',
55
+ options: {
56
+ permissionMode: 'acceptEdits',
57
+ hooks: {
58
+ PostToolUse: [{ matcher: 'Edit|Write', hooks: [logFileChange] }],
59
+ },
60
+ maxTurns: 5,
61
+ },
62
+ })) {
63
+ if ('result' in message) {
64
+ console.log(message.result);
65
+ }
66
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 09-hooks-block-etc.js
3
+ *
4
+ * Equivalent of the official "block writes to /etc" hooks example.
5
+ *
6
+ * Official version:
7
+ * const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
8
+ * const preInput = input as PreToolUseHookInput;
9
+ * const toolInput = preInput.tool_input as Record<string, unknown>;
10
+ * const filePath = toolInput?.file_path as string;
11
+ * if (filePath?.startsWith("/etc")) {
12
+ * return {
13
+ * systemMessage: "Remember: system directories like /etc are protected.",
14
+ * hookSpecificOutput: {
15
+ * hookEventName: preInput.hook_event_name,
16
+ * permissionDecision: "deny",
17
+ * permissionDecisionReason: "Writing to /etc is not allowed"
18
+ * }
19
+ * };
20
+ * }
21
+ * return {};
22
+ * };
23
+ *
24
+ * This elwood version blocks writes to /etc and shows a system message.
25
+ *
26
+ * Prerequisites:
27
+ * - ANTHROPIC_API_KEY set in environment
28
+ * - Claude Code CLI installed
29
+ *
30
+ * Run:
31
+ * node examples/09-hooks-block-etc.js
32
+ */
33
+
34
+ import { query } from '../src/index.js';
35
+
36
+ // Block writes to /etc system directory
37
+ const blockEtcWrites = async (input, _toolUseID, { signal }) => {
38
+ const toolInput = input.tool_input;
39
+ const filePath = typeof toolInput === 'object' ? toolInput?.file_path : undefined;
40
+
41
+ if (typeof filePath === 'string' && filePath.startsWith('/etc')) {
42
+ return {
43
+ // Top-level field: message shown to the user
44
+ systemMessage: 'Remember: system directories like /etc are protected.',
45
+ // hookSpecificOutput: block the operation
46
+ hookSpecificOutput: {
47
+ hookEventName: input.hook_event_name,
48
+ permissionDecision: 'deny',
49
+ permissionDecisionReason: 'Writing to /etc is not allowed',
50
+ },
51
+ };
52
+ }
53
+ return {};
54
+ };
55
+
56
+ for await (const message of query({
57
+ prompt: 'Write a config file to /etc/myapp.conf',
58
+ options: {
59
+ hooks: {
60
+ PreToolUse: [{ matcher: 'Write|Edit', hooks: [blockEtcWrites] }],
61
+ },
62
+ maxTurns: 3,
63
+ },
64
+ })) {
65
+ if (message.type === 'assistant' || message.type === 'result') {
66
+ console.log(message);
67
+ }
68
+ }