This directory contains automated hooks that manage data integrity, backups, and user feedback for the Fluent language learning system.
Hooks ensure your learning data is:
- ✅ Always backed up - Multiple backup strategies prevent data loss
- ✅ Validated - JSON structure checked on every save
- ✅ Tracked - Session stats displayed automatically
- ✅ Safe - Backups created before risky operations (compaction)
Triggered: After every Write/Edit operation on data files
What it does:
- Checks if the modified file is in
data/*.json - Validates JSON structure using Python's JSON parser
- Creates timestamped backup:
data/file.json.backup-20231117-143022 - Shows success message or warning if JSON is invalid
Example output:
[Fluent] ✓ Data saved and validated: data/learner-profile.json
[Fluent] 💾 Backup created: data/learner-profile.json.backup-20231117-143022
Error handling:
- Invalid JSON triggers exit code 2, blocking the operation and alerting Claude
- Error message shown:
[Fluent] ⚠️ WARNING: Invalid JSON in data/file.json
Triggered: When practice session ends (user exits Claude Code)
What it does:
- Creates daily backup directory:
.backups/YYYYMMDD/ - Copies all
data/*.jsonfiles to the backup folder - Reads
learner-profile.jsonto display session summary - Shows current streak and total sessions
Example output:
[Fluent] 📦 Session backup created: .backups/20231117/
[Fluent] 💾 Files backed up: learner-profile.json, progress-db.json, mistakes-db.json
[Fluent] 🔥 Current streak: 7 days
[Fluent] 📊 Total sessions: 42
[Fluent] 👋 Great work today!
Backup location: .backups/YYYYMMDD/ (excluded from git)
Triggered: When Claude Code starts a new session
What it does:
- Checks if
data/learner-profile.jsonexists - If not found, prompts user to run
/setup - If found, displays:
- Welcome message with learner's name
- Target language and current/target level
- Current streak
- Checks
spaced-repetition.jsonfor due reviews - Alerts user if reviews are due today
Example output (first time):
[Fluent] 🌍 Welcome to Fluent - The AI Language Learning Kit!
[Fluent] 📝 Run /setup to create your personalized learning profile
Example output (returning user):
[Fluent] 🌍 Welcome back, Mohammad!
[Fluent] 📚 Learning: Spanish
[Fluent] 🎯 Level: A2 → B1
[Fluent] 🔥 Streak: 12 days
[Fluent] 📅 15 items due for review today - Run /review!
Triggered: Before conversation is compacted (manual /compact or auto-compact)
What it does:
- Creates safety backup directory:
.backups/precompact/ - Copies all
data/*.jsonfiles - Shows confirmation message
Example output:
[Fluent] 🔒 Pre-compact backup saved
Purpose: Ensures data safety before potentially destructive operations
Fluent supports two hook registration paths so the same scripts work whether you cloned the repo or installed it as a plugin:
| Install path | Where hooks are registered | Env var used in commands |
|---|---|---|
| Git clone | .claude/settings.json |
$CLAUDE_PROJECT_DIR |
| Plugin install | .claude/hooks/hooks.json (referenced from plugin.json) |
$CLAUDE_PLUGIN_ROOT (with $CLAUDE_PROJECT_DIR fallback) |
Both paths point at the same Python scripts under .claude/hooks/. The scripts themselves resolve the runtime data directory via fluent_paths.py — $FLUENT_DATA_DIR → ./data/ → ~/.claude/fluent-data/.
Clone-mode .claude/settings.json example:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-data.py"
}
]
}
]
}
}Plugin-mode .claude/hooks/hooks.json example (identical structure, different env var):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR}}/.claude/hooks/validate-data.py"
}
]
}
]
}
}- Event occurs (e.g., file is written)
- Claude Code triggers hook based on matcher pattern
- Script receives JSON input via stdin:
{ "session_id": "abc123", "tool_name": "Write", "tool_input": { "file_path": "data/learner-profile.json", "content": "..." } } - Script processes input and performs actions
- Script exits with status code:
0= Success (stdout shown in verbose mode)2= Blocking error (stderr shown to Claude)- Other = Non-blocking error (logged)
| Exit Code | Behavior | When to Use |
|---|---|---|
0 |
Success, continue normally | Validation passed, backup created |
2 |
Block operation, show stderr to Claude | Invalid JSON, critical error |
| Other | Log error, continue anyway | Non-critical warning |
Fluent uses a multi-layered backup system:
- Location:
data/*.json.backup-YYYYMMDD-HHMMSS - Created: Every time a data file is modified
- Retention: Manual cleanup (keeps all versions)
- Purpose: Granular version history
- Location:
.backups/YYYYMMDD/ - Created: When session ends
- Retention: Manual cleanup (one snapshot per day)
- Purpose: Daily checkpoints
- Location:
.backups/precompact/ - Created: Before conversation compaction
- Retention: Overwritten on each compact
- Purpose: Rollback point for risky operations
All backup directories are excluded from git via .gitignore
Edit validate-data.py to add custom validation logic:
# Example: Validate specific field exists
if file_path == "data/learner-profile.json":
if "learner" not in data or "target_language" not in data["learner"]:
print("[Fluent] ⚠️ Missing required field: target_language", file=sys.stderr)
sys.exit(2) # Block operationEdit session-end.py to add custom analytics:
# Example: Calculate accuracy trend
progress_path = Path("data/progress-db.json")
if progress_path.exists():
with open(progress_path, 'r') as f:
progress = json.load(f)
accuracy = progress.get("overall_stats", {}).get("accuracy_rate", 0)
print(f"[Fluent] 📈 Overall accuracy: {accuracy:.1%}")To add a new hook type:
- Create script in
.claude/hooks/your-hook.py - Make it executable:
chmod +x .claude/hooks/your-hook.py - Add to settings.json:
{ "hooks": { "YourHookEvent": [ { "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/your-hook.py" } ] } ] } }
Run Claude Code with debug flag:
claude --debugThis shows detailed hook execution logs:
[DEBUG] Executing hooks for PostToolUse:Write
[DEBUG] Hook command: .claude/hooks/validate-data.py
[DEBUG] Hook completed with status 0
Enable verbose mode during session:
- Press Ctrl+O to toggle transcript mode
- Shows all hook stdout/stderr output
You can test hooks directly:
# Test validate-data hook
echo '{"tool_name":"Write","tool_input":{"file_path":"data/test.json"}}' | .claude/hooks/validate-data.py
# Test session-start hook
echo '{}' | .claude/hooks/session-start.py| Hook Event | When It Fires | Use Case |
|---|---|---|
PostToolUse |
After Write/Edit/Read/etc | Data validation, backups |
SessionEnd |
When session ends | Cleanup, summaries, backups |
SessionStart |
When session starts | Welcome messages, stats |
PreCompact |
Before compaction | Safety backups |
UserPromptSubmit |
Before processing user input | Prompt validation |
PreToolUse |
Before tool execution | Permission checks |
Problem: Hook doesn't execute Solution:
- Check hook is registered:
cat .claude/settings.json | grep hooks - Verify script is executable:
ls -la .claude/hooks/ - Test script manually (see "Test Hooks Manually" above)
Problem: [Fluent] ⚠️ WARNING: Invalid JSON
Solution:
- Check the last backup:
ls -t data/*.backup-* | head -1 - Validate JSON:
python3 -m json.tool data/file.json - Restore from backup if needed:
cp data/file.json.backup-XXXXXX data/file.json
Problem: Permission denied when running hook
Solution:
chmod +x .claude/hooks/*.pyProblem: Hook times out (default 60s) Solution: Increase timeout in settings.json:
{
"type": "command",
"command": "...",
"timeout": 120
}