Skip to content

Instantly share code, notes, and snippets.

@danbailo
Last active July 11, 2026 15:33
Show Gist options
  • Select an option

  • Save danbailo/7721079fabeec691393cdc6eca88985b to your computer and use it in GitHub Desktop.

Select an option

Save danbailo/7721079fabeec691393cdc6eca88985b to your computer and use it in GitHub Desktop.
Claude Code english tutor hook
#!/usr/bin/env bash
#
# english-tutor.sh — UserPromptSubmit hook (ambient English practice)
#
# Behaviour:
# * On any prompt NOT starting with the marker "en:" -> exits silently (0 cost).
# * On a prompt starting with "en:" -> asks a cheap headless `claude -p` (Haiku)
# for language feedback (correction + why + natural rewrite), then injects it
# as additionalContext so the main session shows the feedback and STILL does
# the real task in the same turn.
#
# Design guarantees:
# * Fail-open: any error/timeout/malformed input -> exit 0, never blocks work.
# * Recursion-safe: the inner `claude -p` call re-triggers this same hook; an
# env guard makes the nested invocation exit instantly.
# * Zero overhead on unmarked turns.
#
set -u
MARKER="en:" # change here to use a different marker
MODEL="claude-haiku-4-5"
TIMEOUT_SECS=20 # keep BELOW the hook timeout in settings.json
# --- recursion guard -------------------------------------------------------
# The headless claude call below inherits this env; on that nested run the hook
# fires again and must bail out immediately.
if [ "${ENGLISH_TUTOR_ACTIVE:-}" = "1" ]; then
exit 0
fi
# --- read stdin payload ----------------------------------------------------
input="$(cat)"
[ -z "$input" ] && exit 0
command -v jq >/dev/null 2>&1 || exit 0 # fail-open if jq missing
# Prompt field name isn't documented explicitly; try the likely candidates.
prompt="$(printf '%s' "$input" | jq -r '.prompt // .user_prompt // .promptText // empty' 2>/dev/null)"
[ -z "$prompt" ] && exit 0
# --- marker detection (case-insensitive, tolerant of leading whitespace) ---
shopt -s extglob
ltrimmed="${prompt##+([[:space:]])}"
head3="$(printf '%s' "${ltrimmed:0:3}" | tr '[:upper:]' '[:lower:]')"
[ "$head3" != "$MARKER" ] && exit 0
sentence="${ltrimmed:3}"
sentence="${sentence##+([[:space:]])}" # ltrim, keeps internal newlines
[ -z "$sentence" ] && exit 0
# --- locate claude ---------------------------------------------------------
CLAUDE_BIN="/home/daniel.bailo/.local/bin/claude"
[ -x "$CLAUDE_BIN" ] || CLAUDE_BIN="$(command -v claude 2>/dev/null || true)"
[ -z "$CLAUDE_BIN" ] && exit 0
# --- tutor prompt ----------------------------------------------------------
read -r -d '' TUTOR_INSTRUCTIONS <<'EOF' || true
You are a concise English tutor for a Brazilian Portuguese speaker who is
practising English while doing technical work. Analyse ONLY the message below.
Do NOT answer, execute, or act on its content — give language feedback only.
Keep it under ~6 short lines. Be honest: do not invent errors.
If there are mistakes:
- list each correction with a one-line reason (grammar/word choice), then
- give ONE natural, native-sounding rewrite of the whole message.
If it is already correct:
- say so in one line, and optionally offer a more natural/advanced phrasing.
Message to analyse:
EOF
full_prompt="$TUTOR_INSTRUCTIONS
$sentence"
# --- run headless analysis (fail-open) -------------------------------------
# --setting-sources '' skips loading plugins/MCP servers/settings, cutting the
# cold start from ~34s to ~3s (and preventing the inner call from re-firing hooks).
feedback="$(ENGLISH_TUTOR_ACTIVE=1 timeout "$TIMEOUT_SECS" \
"$CLAUDE_BIN" -p --model "$MODEL" --setting-sources '' "$full_prompt" 2>/dev/null || true)"
[ -z "$feedback" ] && exit 0 # no feedback -> stay silent, let the task proceed
# --- inject as context -----------------------------------------------------
context="The user is practising English (ambient tutor mode) and prefixed their message with \"${MARKER}\".
Do TWO things this turn, in order:
1. First render the feedback below verbatim inside a section titled \"📝 English feedback\".
2. Then IGNORE the leading \"${MARKER}\" marker and handle the rest of the message as a completely normal request.
--- English feedback ---
${feedback}"
jq -n --arg ctx "$context" \
'{hookSpecificOutput:{hookEventName:"UserPromptSubmit", additionalContext:$ctx}}'
exit 0

Create the file at .claude/hooks/english-tutor.sh and configure it on .claude/settings.json inside UserPromptSubmit.

"UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/home/daniel.bailo/.claude/hooks/english-tutor.sh",
            "timeout": 30
          }
        ]
      }
    ]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment