|
#!/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 |