[BUG] Hooks not saving

Status Fixed / completed
Maintainer reply ✓ Yes — dicksontsai
Activity 7 comments · opened Jul 13, 2025 · closed Jul 14, 2025
💡 Likely answer: A maintainer (dicksontsai, collaborator) responded on this thread — see the highlighted reply below.

Environment

  • Platform (select one):
  • [ ] Anthropic API
  • [ ] AWS Bedrock
  • [ ] Google Vertex AI
  • [ x] Other: local macos
  • Claude CLI version: v1.0.51
  • Operating System: macOS 15.5
  • Terminal: ghostty

Bug Description

Having an inordinate amount of trouble trying to get hooks to work. When I add, I see the hook in my config, but then if I confirm via /hooks, the matcher etc is now gone.

Steps to Reproduce

  1. Add a hook via /hooks that results in:
"hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "deno run --allow-read --allow-write /Users/rcogley/.claude/aichaku/hooks/aichaku-hooks.ts sensitive-file-guard"
          }
        ]
      }
    ]
  }
  1. Esc, esc to REPL and it says it saved the hook.
  2. Was told by chat support that I need to confirm the hook in /hooks so I do that, but starting from the matcher, /hooks says I don't have any configured....

Expected Behavior

I can save and also confirm a hook via /hooks

Actual Behavior

When I save, then go and look at the config, then go back to /hooks, even after a restart of claude code, there is nothing to be seen at all. HOwever, the hook is still in the config.

View original on GitHub ↗

7 Comments

RickCogley · 1 year ago

I wonder, the config files are symlinked. Would this cause trouble?

RickCogley · 1 year ago

Because my ~/.claude/settings.json is a symlink, I tested the theory of adding the test hook to my project .claude/settings.json, because that is just a file. It still does not work after that change. Further, though I have no Stop command configured, debug apparently shows it's looking for a Stop command (but this could be that it is just being verbose, saying "you don't have a stop command"):

> [DEBUG] Executing hooks for Stop
  [DEBUG] Getting matching hook commands for Stop with query: undefined
  [DEBUG] Found 0 hook matchers in settings
  [DEBUG] No match query, returning all 0 hooks
  [DEBUG] Found 0 hook commands to execute
RickCogley · 1 year ago

Also, an aside, but the language in "where should this hook be saved" could be polished up. Inconsistent labels.

<img width="2886" height="1476" alt="Image" src="https://github.com/user-attachments/assets/de5ad0df-e83d-444e-8b04-13c6ef53ccb1" />

RickCogley · 1 year ago

Ok, after debugging, I discovered the hooks were configured correctly but failing silently due to missing permissions and lack of proper error handling. Here's what fixed it:

Root Cause

  1. Missing write permissions - The TypeScript hook only had --allow-read but was trying to write to a log file
  2. No error output - Hooks were failing silently without any visible feedback
  3. Exit codes - Hooks need to use specific exit codes for Claude Code to show output

Solution

The key changes were to the TypeScript hook command and adding proper error handling:

# In .claude/settings.json (or ~/.claude/settings.json)
{
    "hooks": {
      "PreToolUse": [
        {
          "matcher": "Write|Edit|MultiEdit",
          "hooks": [
            {
              "type": "command",
-           "command": "deno run --allow-read /path/to/hook.ts"
+           "command": "deno run --allow-read --allow-write /path/to/hook.ts"
            }
          ]
        }
      ]
    }
}

And in the TypeScript hook itself, adding proper error handling and exit codes:

// aichaku-hooks.ts
#!/usr/bin/env -S deno run --allow-read --allow-write

+// Debug logging function - writes to stderr which shows with exit code 2
+function debug(message: string): void {
+  const timestamp = new Date().toISOString();
+  const logMessage = `[${timestamp}] hook: ${message}\n`;
+  Deno.stderr.writeSync(new TextEncoder().encode(logMessage));
+}

async function sensitiveFileGuard(input: HookInput): Promise<void> {
+  debug(`Checking file: ${input.tool_input?.file_path}`);
const filePath = input.tool_input?.file_path;

    if (filePath && typeof filePath === 'string') {
      const sensitivePatterns = [/\.env$/, /\.env\.\w+$/, /secrets/i];

      if (sensitivePatterns.some(pattern => pattern.test(filePath))) {
        console.log('⚠️  Modifying potentially sensitive file - proceed with caution');
-     Deno.exit(0);  // Silent exit
+     Deno.exit(2);  // Exit code 2 shows stderr to model and blocks tool
      }
    }
}

// Error handling for stdin parsing
  try {
    const input = await readInput();
    // ... process hook
  } catch (e) {
  +  console.error(`Failed to read input: ${e}`);
  +  Deno.exit(2);  // Show error and block
}

Working Example

With these changes, the hooks I was going for now work properly:

🪴 Creating .env.test file...
❌ Write operation blocked by hook:
⚠️ Modifying potentially sensitive file - proceed with caution

Key Findings

  1. Exit codes matter:
  • Exit 0 = silent (no output shown)
  • Exit 2 = show stderr to model and block tool call
  • Other codes = show stderr to user but continue
  1. Permissions are critical - Always include necessary Deno permissions
  2. Debug logging to stderr is essential for troubleshooting
  3. Despite Anthropic website chatbot insisting you must run everything through /hooks, that's not the case, so long as you restart claude code after you make changes. You need to validate carefully, but, it does work to directly edit the config files.

In the end the hooks feature is working in Claude Code - it just needs proper configuration and error handling to be visible.

Hope this will help others who encounter similar issues with hooks not appearing to work! Sorry for the firedrill!

dicksontsai collaborator · 1 year ago

Thanks for writing up your setup experience!

though I have no Stop command configured, debug apparently shows it's looking for a Stop command

Yes, it's just being verbose. It indicates to the user that that moment is when the hook would trigger.

Inconsistent labels.

The inconsistency is intentional. .claude/settings.json (project-level settings) is not in .gitignore, while .claude/settings.local.json (project-local-level settings) is in .gitignore. This means that if you add a hook to .claude/settings.json and git stage/commit normally, the hooks will then be used for everyone else who checks out your commit.

Despite Anthropic website chatbot insisting you must run everything through /hooks

The documentation at https://docs.anthropic.com/en/docs/claude-code/hooks is authoritative. You can pass in the documentation into either Claude Code or claude.ai for better results. Otherwise, neither Claude Code nor claude.ai will know anything about the hooks feature.

RickCogley · 1 year ago

I've passed those docs and still got back some mean hallucinations about what has to be done, but noted. I got the diff between configs, but the grammar in the screenshot is a little funky. At any rate, thanks!

github-actions[bot] · 1 year ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.