Skip to content

Commit 955eabd

Browse files
dbejarano820claude
andcommitted
feat: smart filtering with rate limits and trivial command skip
Skip teaching triggers for trivial commands (cd, ls, pwd, etc.). Rate limit: 30s minimum between triggers, 12 per session max. First-ever concepts bypass rate limit but not session cap. Session state tracked in ~/.code-sensei/session-state.json. Closes DOJ-2433 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b96a272 commit 955eabd

File tree

3 files changed

+129
-8
lines changed

3 files changed

+129
-8
lines changed

scripts/session-stop.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
PROFILE_DIR="$HOME/.code-sensei"
66
PROFILE_FILE="$PROFILE_DIR/profile.json"
77
SESSION_LOG="$PROFILE_DIR/sessions.log"
8+
SESSION_STATE="$PROFILE_DIR/session-state.json"
89
TODAY=$(date -u +%Y-%m-%d)
910

1011
if [ ! -f "$PROFILE_FILE" ]; then
@@ -27,8 +28,12 @@ if command -v jq &> /dev/null; then
2728

2829
# Show gentle reminder if they learned things but didn't recap
2930
if [ "$SESSION_CONCEPTS" -gt 0 ]; then
30-
echo "🥋 You encountered $SESSION_CONCEPTS new concepts this session! Use /code-sensei:recap next time for a full summary."
31+
echo "You encountered $SESSION_CONCEPTS new concepts this session! Use /code-sensei:recap next time for a full summary."
3132
fi
3233
fi
3334

35+
# Clean up session state files
36+
rm -f "$SESSION_STATE"
37+
rm -f "$PROFILE_DIR/.jq-warned"
38+
3439
exit 0

scripts/track-code-change.sh

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
PROFILE_DIR="$HOME/.code-sensei"
77
PROFILE_FILE="$PROFILE_DIR/profile.json"
88
CHANGES_LOG="$PROFILE_DIR/session-changes.jsonl"
9+
SESSION_STATE="$PROFILE_DIR/session-state.json"
10+
11+
# Rate limiting constants
12+
RATE_LIMIT_INTERVAL=30 # minimum seconds between teaching triggers
13+
SESSION_CAP=12 # maximum teaching triggers per session
914

1015
# Read hook input from stdin
1116
INPUT=$(cat)
@@ -62,15 +67,57 @@ if command -v jq &> /dev/null; then
6267
fi
6368
fi
6469

65-
# Always inject teaching context after code changes
70+
# --- Rate limiting ---
71+
NOW=$(date +%s)
72+
73+
# Read current session state (graceful default if missing)
74+
if [ -f "$SESSION_STATE" ]; then
75+
LAST_TRIGGER=$(jq -r '.last_trigger_time // 0' "$SESSION_STATE" 2>/dev/null || echo "0")
76+
TRIGGER_COUNT=$(jq -r '.trigger_count // 0' "$SESSION_STATE" 2>/dev/null || echo "0")
77+
else
78+
LAST_TRIGGER=0
79+
TRIGGER_COUNT=0
80+
fi
81+
82+
# Enforce session cap (applies to everyone, including first-ever concepts)
83+
if [ "$TRIGGER_COUNT" -ge "$SESSION_CAP" ]; then
84+
echo "{}"
85+
exit 0
86+
fi
87+
88+
# Enforce minimum interval — first-ever concepts bypass this check only
89+
ELAPSED=$(( NOW - LAST_TRIGGER ))
90+
if [ "$ELAPSED" -lt "$RATE_LIMIT_INTERVAL" ] && [ "$IS_FIRST_EVER" != "true" ]; then
91+
echo "{}"
92+
exit 0
93+
fi
94+
95+
# Update session state
96+
NEW_COUNT=$(( TRIGGER_COUNT + 1 ))
97+
SESSION_START_VAL=""
98+
if [ -f "$SESSION_STATE" ]; then
99+
SESSION_START_VAL=$(jq -r '.session_start // ""' "$SESSION_STATE" 2>/dev/null || echo "")
100+
fi
101+
if [ -z "$SESSION_START_VAL" ]; then
102+
SESSION_START_VAL="$TIMESTAMP"
103+
fi
104+
105+
jq -n \
106+
--argjson last "$NOW" \
107+
--argjson count "$NEW_COUNT" \
108+
--arg start "$SESSION_START_VAL" \
109+
'{"last_trigger_time": $last, "trigger_count": $count, "session_start": $start}' \
110+
> "$SESSION_STATE"
111+
112+
# Build teaching context
66113
BELT=$(jq -r '.belt // "white"' "$PROFILE_FILE" 2>/dev/null || echo "white")
67114

68115
if [ "$IS_FIRST_EVER" = "true" ]; then
69116
# First-time encounter: micro-lesson about the technology
70-
CONTEXT="🥋 CodeSensei micro-lesson trigger: The user just encountered '$TECH' for the FIRST TIME (file: $FILE_PATH). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $TECH is and why it matters for their project. Adapt language to their belt level. Keep it concise and non-intrusive — weave it naturally into your response, don't stop everything for a lecture."
117+
CONTEXT="CodeSensei micro-lesson trigger: The user just encountered '$TECH' for the FIRST TIME (file: $FILE_PATH). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $TECH is and why it matters for their project. Adapt language to their belt level. Keep it concise and non-intrusive — weave it naturally into your response, don't stop everything for a lecture."
71118
else
72119
# Already-seen technology: inline insight about the specific change
73-
CONTEXT="🥋 CodeSensei inline insight: Claude just used '$TOOL_NAME' on '$FILE_PATH' ($TECH). The user's belt level is '$BELT'. Provide a brief 1-2 sentence explanation of what this change does and why, adapted to their belt level. Keep it natural and non-intrusive — weave it into your response as a quick teaching moment."
120+
CONTEXT="CodeSensei inline insight: Claude just used '$TOOL_NAME' on '$FILE_PATH' ($TECH). The user's belt level is '$BELT'. Provide a brief 1-2 sentence explanation of what this change does and why, adapted to their belt level. Keep it natural and non-intrusive — weave it into your response as a quick teaching moment."
74121
fi
75122

76123
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"$CONTEXT\"}}"

scripts/track-command.sh

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
PROFILE_DIR="$HOME/.code-sensei"
77
PROFILE_FILE="$PROFILE_DIR/profile.json"
88
COMMANDS_LOG="$PROFILE_DIR/session-commands.jsonl"
9+
SESSION_STATE="$PROFILE_DIR/session-state.json"
10+
11+
# Rate limiting constants
12+
RATE_LIMIT_INTERVAL=30 # minimum seconds between teaching triggers
13+
SESSION_CAP=12 # maximum teaching triggers per session
14+
15+
# Trivial commands that should never generate teaching triggers
16+
TRIVIAL_COMMANDS="cd ls pwd clear echo cat which man help exit history alias type file wc whoami hostname uname true false"
917

1018
# Read hook input from stdin
1119
INPUT=$(cat)
@@ -18,6 +26,25 @@ if command -v jq &> /dev/null; then
1826
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // "unknown"')
1927
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
2028

29+
# Extract the base command (first word, ignoring leading whitespace)
30+
BASE_CMD=$(echo "$COMMAND" | sed 's/^[[:space:]]*//' | awk '{print $1}' | sed 's|.*/||')
31+
32+
# Check if the command is trivial — skip teaching triggers entirely
33+
IS_TRIVIAL="false"
34+
for trivial in $TRIVIAL_COMMANDS; do
35+
if [ "$BASE_CMD" = "$trivial" ]; then
36+
IS_TRIVIAL="true"
37+
break
38+
fi
39+
done
40+
41+
if [ "$IS_TRIVIAL" = "true" ]; then
42+
# Log it but emit no teaching context
43+
echo "{\"timestamp\":\"$TIMESTAMP\",\"command\":\"$(echo "$COMMAND" | head -c 200)\",\"concept\":\"\",\"skipped\":\"trivial\"}" >> "$COMMANDS_LOG"
44+
echo "{}"
45+
exit 0
46+
fi
47+
2148
# Detect what kind of command this is for concept tracking
2249
CONCEPT=""
2350
case "$COMMAND" in
@@ -64,20 +91,62 @@ if command -v jq &> /dev/null; then
6491
fi
6592
fi
6693

67-
# Always inject teaching context after commands
94+
# --- Rate limiting ---
95+
NOW=$(date +%s)
96+
97+
# Read current session state (graceful default if missing)
98+
if [ -f "$SESSION_STATE" ]; then
99+
LAST_TRIGGER=$(jq -r '.last_trigger_time // 0' "$SESSION_STATE" 2>/dev/null || echo "0")
100+
TRIGGER_COUNT=$(jq -r '.trigger_count // 0' "$SESSION_STATE" 2>/dev/null || echo "0")
101+
else
102+
LAST_TRIGGER=0
103+
TRIGGER_COUNT=0
104+
fi
105+
106+
# Enforce session cap (applies to everyone, including first-ever concepts)
107+
if [ "$TRIGGER_COUNT" -ge "$SESSION_CAP" ]; then
108+
echo "{}"
109+
exit 0
110+
fi
111+
112+
# Enforce minimum interval — first-ever concepts bypass this check only
113+
ELAPSED=$(( NOW - LAST_TRIGGER ))
114+
if [ "$ELAPSED" -lt "$RATE_LIMIT_INTERVAL" ] && [ "$IS_FIRST_EVER" != "true" ]; then
115+
echo "{}"
116+
exit 0
117+
fi
118+
119+
# Update session state
120+
NEW_COUNT=$(( TRIGGER_COUNT + 1 ))
121+
SESSION_START_VAL=""
122+
if [ -f "$SESSION_STATE" ]; then
123+
SESSION_START_VAL=$(jq -r '.session_start // ""' "$SESSION_STATE" 2>/dev/null || echo "")
124+
fi
125+
if [ -z "$SESSION_START_VAL" ]; then
126+
SESSION_START_VAL="$TIMESTAMP"
127+
fi
128+
129+
jq -n \
130+
--argjson last "$NOW" \
131+
--argjson count "$NEW_COUNT" \
132+
--arg start "$SESSION_START_VAL" \
133+
'{"last_trigger_time": $last, "trigger_count": $count, "session_start": $start}' \
134+
> "$SESSION_STATE"
135+
136+
# Build teaching context
68137
BELT=$(jq -r '.belt // "white"' "$PROFILE_FILE" 2>/dev/null || echo "white")
69138
# Sanitize command for JSON (remove quotes and special chars)
70139
SAFE_CMD=$(echo "$COMMAND" | head -c 80 | tr '"' "'" | tr '\\' '/')
71140

72141
if [ "$IS_FIRST_EVER" = "true" ] && [ -n "$CONCEPT" ]; then
73142
# First-time encounter: micro-lesson about the concept
74-
CONTEXT="🥋 CodeSensei micro-lesson trigger: The user just encountered '$CONCEPT' for the FIRST TIME (command: $SAFE_CMD). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $CONCEPT means and why it matters. Adapt language to their belt level. Keep it concise and non-intrusive."
143+
CONTEXT="CodeSensei micro-lesson trigger: The user just encountered '$CONCEPT' for the FIRST TIME (command: $SAFE_CMD). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $CONCEPT means and why it matters. Adapt language to their belt level. Keep it concise and non-intrusive."
75144
elif [ -n "$CONCEPT" ]; then
76145
# Already-seen concept: brief inline insight about this specific command
77-
CONTEXT="🥋 CodeSensei inline insight: Claude just ran a '$CONCEPT' command ($SAFE_CMD). The user's belt level is '$BELT'. Provide a brief 1-sentence explanation of what this command does, adapted to their belt level. Keep it natural and non-intrusive."
146+
CONTEXT="CodeSensei inline insight: Claude just ran a '$CONCEPT' command ($SAFE_CMD). The user's belt level is '$BELT'. Provide a brief 1-sentence explanation of what this command does, adapted to their belt level. Keep it natural and non-intrusive."
78147
else
79148
# Unknown command type: still provide a brief hint
80-
CONTEXT="🥋 CodeSensei inline insight: Claude just ran a shell command ($SAFE_CMD). The user's belt level is '$BELT'. If this command is educational, briefly explain what it does in 1 sentence. If trivial, skip the explanation."
149+
CONTEXT="CodeSensei inline insight: Claude just ran a shell command ($SAFE_CMD). The user's belt level is '$BELT'. If this command is educational, briefly explain what it does in 1 sentence. If trivial, skip the explanation."
81150
fi
82151

83152
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"$CONTEXT\"}}"

0 commit comments

Comments
 (0)