Skip to content

Add PreToolUse hook to block AI writing check#144

Merged
Miyamura80 merged 3 commits intomainfrom
ai-writing-guard
Mar 15, 2026
Merged

Add PreToolUse hook to block AI writing check#144
Miyamura80 merged 3 commits intomainfrom
ai-writing-guard

Conversation

@Miyamura80
Copy link
Owner

Summary

  • Adds a Claude Code PreToolUse hook that intercepts attempts to directly run the em-dash checking script
  • Auto-denies the command and instructs Claude to avoid em dashes instead, since the check runs automatically via pre-commit
  • Adds .claude/settings.json with hook configuration

Test plan

  • Verify Claude Code sessions pick up the new hook from .claude/settings.json
  • Confirm that running the em-dash checker script directly as a Bash command is auto-denied
  • Confirm that other Bash commands (including those mentioning the script name in strings) pass through normally

🤖 Generated with Claude Code

Miyamura80 and others added 2 commits March 15, 2026 01:08
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 15, 2026

Greptile Summary

This PR adds a Claude Code PreToolUse hook (.claude/hooks/enforce-makefile.py) and its wiring in .claude/settings.json to intercept and block direct invocations of check_ai_writing.py, instructing Claude to avoid em dashes instead of running the checker manually. The approach is sound, but there are two issues in the hook script that could affect correctness and reliability.

  • The json.loads(sys.stdin.read()) call on line 23 has no error handling — malformed or empty stdin will raise an unhandled exception, potentially causing unexpected behavior in Claude Code (e.g., silently blocking all Bash commands depending on how hook failures are treated).
  • The regex alternative \./ on line 28 is too broad: re.search will match any ./ in the command string and then allow .* to span unrelated tokens until check_ai_writing.py appears, creating false positive denials for commands like ./lint.sh && echo "check_ai_writing.py passed".

Confidence Score: 3/5

  • The PR is mostly safe to merge but has a regex false-positive and missing error handling that could cause the hook to block legitimate Bash commands or crash.
  • The logic issues in the hook script — an unhandled JSON parse exception and an overly broad regex alternative — could result in either hook crashes or incorrect blocking of unrelated commands. Neither is catastrophic, but both should be fixed before the hook is relied upon.
  • .claude/hooks/enforce-makefile.py needs attention for the error handling and regex issues.

Important Files Changed

Filename Overview
.claude/hooks/enforce-makefile.py New PreToolUse hook that blocks direct invocations of check_ai_writing.py. Two issues: unhandled exception if stdin JSON is malformed (could crash the hook), and the \./ regex alternative can match false positives for commands that start with ./ and mention the script name elsewhere in the string.
.claude/settings.json Adds Claude Code hook configuration wiring the enforce-makefile.py script as a PreToolUse handler for Bash commands. Configuration looks correct.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: .claude/hooks/enforce-makefile.py
Line: 23

Comment:
**Unhandled exception on malformed stdin**

`json.loads(sys.stdin.read())` will raise a `json.JSONDecodeError` (or `ValueError`) if Claude Code sends empty or malformed input, causing the hook to crash with a non-zero exit code. Depending on how Claude Code handles a hook subprocess failure, this could inadvertently block all Bash commands rather than failing open. Wrapping this in a `try/except` and exiting cleanly when input is unexpected would make the hook resilient:

```suggestion
    try:
        hook_input = json.loads(sys.stdin.read())
    except (json.JSONDecodeError, ValueError):
        sys.exit(0)
    command = hook_input.get("tool_input", {}).get("command", "")
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: .claude/hooks/enforce-makefile.py
Line: 28

Comment:
**Regex `\./ ` alternative causes false positives**

The `\./ ` branch in `(uv\s+run\s+python|python3?|\./)` will match any `./` occurrence anywhere in the command string, and `.*` is then free to span across unrelated tokens until `check_ai_writing\.py` is found. For example, a legitimate command like `./lint.sh && echo "check_ai_writing.py passed"` would be incorrectly blocked because `./` matches at the start, and `.*check_ai_writing\.py` swallows the rest of the string.

To fix this, tighten the `\./ ` branch so it only matches when `check_ai_writing.py` directly follows, without any directory separator in between:

```suggestion
    if re.search(r"(uv\s+run\s+python\s+|python3?\s+|\./)check_ai_writing\.py", command):
```

This ensures `./` is only matched when it immediately precedes `check_ai_writing.py`, preventing false positives from commands that happen to start with `./` and separately mention the script name.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 349a88c

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Miyamura80 Miyamura80 merged commit 178c564 into main Mar 15, 2026
11 checks passed
@github-actions github-actions bot deleted the ai-writing-guard branch March 15, 2026 01:13


def main() -> None:
hook_input = json.loads(sys.stdin.read())
Copy link
Contributor

Choose a reason for hiding this comment

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

Unhandled exception on malformed stdin

json.loads(sys.stdin.read()) will raise a json.JSONDecodeError (or ValueError) if Claude Code sends empty or malformed input, causing the hook to crash with a non-zero exit code. Depending on how Claude Code handles a hook subprocess failure, this could inadvertently block all Bash commands rather than failing open. Wrapping this in a try/except and exiting cleanly when input is unexpected would make the hook resilient:

Suggested change
hook_input = json.loads(sys.stdin.read())
try:
hook_input = json.loads(sys.stdin.read())
except (json.JSONDecodeError, ValueError):
sys.exit(0)
command = hook_input.get("tool_input", {}).get("command", "")
Prompt To Fix With AI
This is a comment left during a code review.
Path: .claude/hooks/enforce-makefile.py
Line: 23

Comment:
**Unhandled exception on malformed stdin**

`json.loads(sys.stdin.read())` will raise a `json.JSONDecodeError` (or `ValueError`) if Claude Code sends empty or malformed input, causing the hook to crash with a non-zero exit code. Depending on how Claude Code handles a hook subprocess failure, this could inadvertently block all Bash commands rather than failing open. Wrapping this in a `try/except` and exiting cleanly when input is unexpected would make the hook resilient:

```suggestion
    try:
        hook_input = json.loads(sys.stdin.read())
    except (json.JSONDecodeError, ValueError):
        sys.exit(0)
    command = hook_input.get("tool_input", {}).get("command", "")
```

How can I resolve this? If you propose a fix, please make it concise.


# Block direct invocation of the AI writing check script
# Match only actual script execution, not mentions in strings
if re.search(r"(uv\s+run\s+python|python3?|\./).*check_ai_writing\.py", command):
Copy link
Contributor

Choose a reason for hiding this comment

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

Regex \./ alternative causes false positives

The \./ branch in (uv\s+run\s+python|python3?|\./) will match any ./ occurrence anywhere in the command string, and .* is then free to span across unrelated tokens until check_ai_writing\.py is found. For example, a legitimate command like ./lint.sh && echo "check_ai_writing.py passed" would be incorrectly blocked because ./ matches at the start, and .*check_ai_writing\.py swallows the rest of the string.

To fix this, tighten the \./ branch so it only matches when check_ai_writing.py directly follows, without any directory separator in between:

Suggested change
if re.search(r"(uv\s+run\s+python|python3?|\./).*check_ai_writing\.py", command):
if re.search(r"(uv\s+run\s+python\s+|python3?\s+|\./)check_ai_writing\.py", command):

This ensures ./ is only matched when it immediately precedes check_ai_writing.py, preventing false positives from commands that happen to start with ./ and separately mention the script name.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .claude/hooks/enforce-makefile.py
Line: 28

Comment:
**Regex `\./ ` alternative causes false positives**

The `\./ ` branch in `(uv\s+run\s+python|python3?|\./)` will match any `./` occurrence anywhere in the command string, and `.*` is then free to span across unrelated tokens until `check_ai_writing\.py` is found. For example, a legitimate command like `./lint.sh && echo "check_ai_writing.py passed"` would be incorrectly blocked because `./` matches at the start, and `.*check_ai_writing\.py` swallows the rest of the string.

To fix this, tighten the `\./ ` branch so it only matches when `check_ai_writing.py` directly follows, without any directory separator in between:

```suggestion
    if re.search(r"(uv\s+run\s+python\s+|python3?\s+|\./)check_ai_writing\.py", command):
```

This ensures `./` is only matched when it immediately precedes `check_ai_writing.py`, preventing false positives from commands that happen to start with `./` and separately mention the script name.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant