Advanced Guardrails: - prompt-injection-detector.sh (PreToolUse) - output-validator.sh (PostToolUse heuristics) - claudemd-scanner.sh (SessionStart injection detection) - output-secrets-scanner.sh (PostToolUse secrets leak prevention) Observability & Monitoring: - session-logger.sh (JSONL activity logging) - session-stats.sh (cost tracking & analysis) - guide/observability.md (full documentation) LLM-as-a-Judge Evaluation: - output-evaluator.md agent (Haiku) - /validate-changes command - pre-commit-evaluator.sh (opt-in git hook) Google Agent Whitepaper Integration: - Context Triage Guide (Section 2.2.4) - CLAUDE.md Injection Warning (Section 3.1.3) - Agent Validation Checklist (Section 4.2.4) - MCP Security: Tool Shadowing & Confused Deputy (Section 8.6) - Session vs Memory patterns (Section 3.3.3) Stats: 10 new files, 8 modified, 5 new guide sections Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
235 lines
7.8 KiB
Bash
Executable file
235 lines
7.8 KiB
Bash
Executable file
#!/bin/bash
|
|
# session-stats.sh - Analyze Claude Code session logs
|
|
#
|
|
# Analyzes logs created by session-logger.sh hook and outputs statistics
|
|
# about tool usage, estimated costs, and session patterns.
|
|
#
|
|
# Usage:
|
|
# ./session-stats.sh # Today's summary
|
|
# ./session-stats.sh --range week # Last 7 days
|
|
# ./session-stats.sh --range month # Last 30 days
|
|
# ./session-stats.sh --date 2026-01-14 # Specific date
|
|
# ./session-stats.sh --json # Machine-readable output
|
|
# ./session-stats.sh --project myapp # Filter by project
|
|
#
|
|
# Environment:
|
|
# CLAUDE_LOG_DIR - Log directory (default: ~/.claude/logs)
|
|
#
|
|
# Cost rates (per 1K tokens, configurable):
|
|
# CLAUDE_RATE_INPUT - Input token rate (default: 0.003 for Sonnet)
|
|
# CLAUDE_RATE_OUTPUT - Output token rate (default: 0.015 for Sonnet)
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
# Configuration
|
|
LOG_DIR="${CLAUDE_LOG_DIR:-$HOME/.claude/logs}"
|
|
RATE_INPUT="${CLAUDE_RATE_INPUT:-0.003}"
|
|
RATE_OUTPUT="${CLAUDE_RATE_OUTPUT:-0.015}"
|
|
|
|
# Defaults
|
|
OUTPUT_MODE="human"
|
|
DATE_RANGE="today"
|
|
SPECIFIC_DATE=""
|
|
PROJECT_FILTER=""
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--json)
|
|
OUTPUT_MODE="json"
|
|
shift
|
|
;;
|
|
--range)
|
|
DATE_RANGE="$2"
|
|
shift 2
|
|
;;
|
|
--date)
|
|
SPECIFIC_DATE="$2"
|
|
DATE_RANGE="specific"
|
|
shift 2
|
|
;;
|
|
--project)
|
|
PROJECT_FILTER="$2"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
grep '^#' "$0" | sed 's/^# \?//'
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
echo "Use --help for usage information"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Check if log directory exists
|
|
if [[ ! -d "$LOG_DIR" ]]; then
|
|
if [[ "$OUTPUT_MODE" == "json" ]]; then
|
|
echo '{"error": "No logs found", "log_dir": "'"$LOG_DIR"'"}'
|
|
else
|
|
echo -e "${RED}No logs found in $LOG_DIR${NC}"
|
|
echo "Enable session-logger.sh hook to start collecting logs."
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
# Determine which log files to analyze
|
|
LOG_FILES=()
|
|
|
|
case "$DATE_RANGE" in
|
|
today)
|
|
TODAY=$(date +%Y-%m-%d)
|
|
[[ -f "$LOG_DIR/activity-$TODAY.jsonl" ]] && LOG_FILES+=("$LOG_DIR/activity-$TODAY.jsonl")
|
|
;;
|
|
week)
|
|
for i in {0..6}; do
|
|
DATE=$(date -v-${i}d +%Y-%m-%d 2>/dev/null || date -d "-$i days" +%Y-%m-%d)
|
|
[[ -f "$LOG_DIR/activity-$DATE.jsonl" ]] && LOG_FILES+=("$LOG_DIR/activity-$DATE.jsonl")
|
|
done
|
|
;;
|
|
month)
|
|
for i in {0..29}; do
|
|
DATE=$(date -v-${i}d +%Y-%m-%d 2>/dev/null || date -d "-$i days" +%Y-%m-%d)
|
|
[[ -f "$LOG_DIR/activity-$DATE.jsonl" ]] && LOG_FILES+=("$LOG_DIR/activity-$DATE.jsonl")
|
|
done
|
|
;;
|
|
specific)
|
|
[[ -f "$LOG_DIR/activity-$SPECIFIC_DATE.jsonl" ]] && LOG_FILES+=("$LOG_DIR/activity-$SPECIFIC_DATE.jsonl")
|
|
;;
|
|
esac
|
|
|
|
if [[ ${#LOG_FILES[@]} -eq 0 ]]; then
|
|
if [[ "$OUTPUT_MODE" == "json" ]]; then
|
|
echo '{"error": "No logs found for specified range", "range": "'"$DATE_RANGE"'"}'
|
|
else
|
|
echo -e "${YELLOW}No logs found for range: $DATE_RANGE${NC}"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Combine and filter logs
|
|
COMBINED_LOGS=$(cat "${LOG_FILES[@]}" 2>/dev/null || true)
|
|
|
|
if [[ -n "$PROJECT_FILTER" ]]; then
|
|
COMBINED_LOGS=$(echo "$COMBINED_LOGS" | jq -c "select(.project == \"$PROJECT_FILTER\")" 2>/dev/null || true)
|
|
fi
|
|
|
|
if [[ -z "$COMBINED_LOGS" ]]; then
|
|
if [[ "$OUTPUT_MODE" == "json" ]]; then
|
|
echo '{"error": "No matching logs found"}'
|
|
else
|
|
echo -e "${YELLOW}No matching logs found${NC}"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Calculate statistics
|
|
TOTAL_OPS=$(echo "$COMBINED_LOGS" | wc -l | tr -d ' ')
|
|
TOTAL_TOKENS_IN=$(echo "$COMBINED_LOGS" | jq -s '[.[].tokens.input // 0] | add')
|
|
TOTAL_TOKENS_OUT=$(echo "$COMBINED_LOGS" | jq -s '[.[].tokens.output // 0] | add')
|
|
TOTAL_TOKENS=$((TOTAL_TOKENS_IN + TOTAL_TOKENS_OUT))
|
|
|
|
# Cost calculation
|
|
COST_INPUT=$(echo "scale=4; $TOTAL_TOKENS_IN * $RATE_INPUT / 1000" | bc)
|
|
COST_OUTPUT=$(echo "scale=4; $TOTAL_TOKENS_OUT * $RATE_OUTPUT / 1000" | bc)
|
|
COST_TOTAL=$(echo "scale=4; $COST_INPUT + $COST_OUTPUT" | bc)
|
|
|
|
# Tool breakdown
|
|
TOOL_STATS=$(echo "$COMBINED_LOGS" | jq -s 'group_by(.tool) | map({tool: .[0].tool, count: length}) | sort_by(-.count)')
|
|
|
|
# Session count
|
|
SESSION_COUNT=$(echo "$COMBINED_LOGS" | jq -s '[.[].session_id] | unique | length')
|
|
|
|
# Project breakdown
|
|
PROJECT_STATS=$(echo "$COMBINED_LOGS" | jq -s 'group_by(.project) | map({project: .[0].project, count: length}) | sort_by(-.count)')
|
|
|
|
# Most edited files
|
|
FILE_STATS=$(echo "$COMBINED_LOGS" | jq -s '[.[] | select(.file != null)] | group_by(.file) | map({file: .[0].file, count: length}) | sort_by(-.count) | .[0:10]')
|
|
|
|
# Output
|
|
if [[ "$OUTPUT_MODE" == "json" ]]; then
|
|
jq -n \
|
|
--arg range "$DATE_RANGE" \
|
|
--argjson total_ops "$TOTAL_OPS" \
|
|
--argjson sessions "$SESSION_COUNT" \
|
|
--argjson tokens_in "$TOTAL_TOKENS_IN" \
|
|
--argjson tokens_out "$TOTAL_TOKENS_OUT" \
|
|
--argjson tokens_total "$TOTAL_TOKENS" \
|
|
--arg cost_in "$COST_INPUT" \
|
|
--arg cost_out "$COST_OUTPUT" \
|
|
--arg cost_total "$COST_TOTAL" \
|
|
--argjson tools "$TOOL_STATS" \
|
|
--argjson projects "$PROJECT_STATS" \
|
|
--argjson files "$FILE_STATS" \
|
|
'{
|
|
range: $range,
|
|
summary: {
|
|
total_operations: $total_ops,
|
|
sessions: $sessions,
|
|
tokens: {
|
|
input: $tokens_in,
|
|
output: $tokens_out,
|
|
total: $tokens_total
|
|
},
|
|
estimated_cost: {
|
|
input: ($cost_in | tonumber),
|
|
output: ($cost_out | tonumber),
|
|
total: ($cost_total | tonumber),
|
|
currency: "USD"
|
|
}
|
|
},
|
|
tools: $tools,
|
|
projects: $projects,
|
|
top_files: $files
|
|
}'
|
|
else
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${CYAN} Claude Code Session Statistics - $DATE_RANGE${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
echo -e "${BLUE}Summary${NC}"
|
|
echo " Total operations: $TOTAL_OPS"
|
|
echo " Sessions: $SESSION_COUNT"
|
|
echo ""
|
|
|
|
echo -e "${BLUE}Token Usage${NC}"
|
|
printf " Input tokens: %'d\n" "$TOTAL_TOKENS_IN"
|
|
printf " Output tokens: %'d\n" "$TOTAL_TOKENS_OUT"
|
|
printf " Total tokens: %'d\n" "$TOTAL_TOKENS"
|
|
echo ""
|
|
|
|
echo -e "${BLUE}Estimated Cost (Sonnet rates)${NC}"
|
|
printf " Input: \$%.4f\n" "$COST_INPUT"
|
|
printf " Output: \$%.4f\n" "$COST_OUTPUT"
|
|
printf " ${GREEN}Total: \$%.4f${NC}\n" "$COST_TOTAL"
|
|
echo ""
|
|
|
|
echo -e "${BLUE}Tools Used${NC}"
|
|
echo "$TOOL_STATS" | jq -r '.[] | " \(.tool): \(.count)"'
|
|
echo ""
|
|
|
|
echo -e "${BLUE}Projects${NC}"
|
|
echo "$PROJECT_STATS" | jq -r '.[] | " \(.project): \(.count)"'
|
|
echo ""
|
|
|
|
echo -e "${BLUE}Most Edited Files${NC}"
|
|
echo "$FILE_STATS" | jq -r '.[] | " \(.file | split("/")[-1]): \(.count)"' | head -5
|
|
echo ""
|
|
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
|
echo -e "Logs: $LOG_DIR"
|
|
echo -e "Rate config: \$${RATE_INPUT}/1K in, \$${RATE_OUTPUT}/1K out"
|
|
echo ""
|
|
fi
|