Serena usage statistics from log files

Serena is an MCP server that gives AI coding agents semantic understanding of your codebase. Instead of reading raw files, the agent can navigate symbols, find references, rename across the project, and use language-server-backed tools -- all through the Model Context Protocol. I use it with Claude Code, where it runs as a local MCP server alongside each session.

Serena writes detailed logs to ~/.serena/logs/, organized by date. Each session produces a text file containing timestamps, tool calls with parameters, results, and execution times. There is no built-in way to get usage statistics from these logs, but the format is structured enough that a simple Python script can extract useful insights. I asked Claude Code to build one, and after a few iterations this is what came out.

The log format

Each log file corresponds to one MCP server session. The filename encodes the start time and process ID: mcp_20260325-175343_2802043.txt. Inside, every line follows Python's standard logging format:

INFO  2026-03-25 17:54:45,042 [MainThread] mcp.server.lowlevel.server:_handle_request:720 - Processing request of type CallToolRequest
INFO  2026-03-25 17:54:45,043 [MainThread] serena.task_executor:issue_task:192 - Scheduling Task-2:ReadMemoryTool
INFO  2026-03-25 17:54:45,046 [Task-2:ReadMemoryTool] serena.tools.tools_base:_log_tool_application:222 - read_memory: memory_name='some-memory-name'
INFO  2026-03-25 17:54:45,047 [Task-2:ReadMemoryTool] serena.task_executor:stop:336 - Task-2:ReadMemoryTool completed in 0.001 seconds

Each tool call goes through the same lifecycle: CallToolRequest -> Scheduling Task-N:ToolName -> parameters logged -> result logged -> completed in X seconds. A handful of regular expressions is enough to extract tool names, timestamps, durations, and result sizes from this.

What the script reports

Sessions where Serena started but no tool was ever called are filtered out -- these are just idle MCP server instances.

Overview and daily activity:

============================================================
  Serena Usage Overview
============================================================
  Total sessions:   15 (63 idle skipped)
  Total tool calls: 94
  Serena versions:  0.1.4 (44 builds)

============================================================
  Sessions per Day
============================================================
  2026-03-22    2 sessions     6 tool calls  ######
  2026-03-25    4 sessions    44 tool calls  ############################################

Tool usage -- which Serena tools are called most often, with average and maximum execution times. GetSymbolsOverview and FindSymbol dominate, which makes sense -- they are the primary way an agent explores code structure before diving into specifics.

============================================================
  Tool Usage (top 20)
============================================================
  GetSymbolsOverview               28  avg 0.064s  max 0.354s       ############################
  FindSymbol                       27  avg 0.077s  max 0.638s       ###########################
  SearchForPattern                 17  avg 0.229s  max 2.940s       #################
  ListDir                           9  avg 0.007s  max 0.029s       #########
  FindFile                          9  avg 0.010s  max 0.027s       #########

Result sizes -- since every tool result is logged, the script can measure the character count per response. This approximates how much context window each tool consumes. FindSymbol and SearchForPattern are the expensive ones, while GetSymbolsOverview stays compact at ~360 chars per call.

============================================================
  Result Sizes (context window cost)
============================================================
  Total result data: 135.4k chars

  FindSymbol                        73.6k total  avg   2.7k  max  16.6k  (27 calls)
  SearchForPattern                  47.3k total  avg   2.8k  max  24.2k  (17 calls)
  GetSymbolsOverview                10.2k total  avg    363  max   3.4k  (28 calls)
  ListDir                            3.0k total  avg    335  max   1.6k  (9 calls)

The script also reports projects, session durations, language server startup times (Vue: 4s avg, Python: 0.3s), failed tool calls (9% failure rate, mostly FindSymbol hitting missing files), and hourly activity.

Claude Code statusline

The Claude Code statusline is a customizable bar at the bottom of the terminal that runs a shell script after each assistant message. Claude Code sends JSON session data to stdin, and whatever the script prints becomes the status bar content -- ANSI colors included.

My setup displays the current model, a context window progress bar, and rate limit information when available.

Configuration

The statusline is enabled in ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/statusline-command.sh"
  }
}

The script at ~/.claude/statusline-command.sh:

#!/usr/bin/env bash
input=$(cat)

CYN='\033[1;36m' GRN='\033[0;32m' YLW='\033[0;33m' MAG='\033[0;35m' RST='\033[0m'

eval "$(echo "$input" | jq -r '
  @sh "model=\(.model.display_name // "Claude")",
  @sh "used=\(.context_window.used_percentage // "")",
  @sh "rl_pct=\(.rate_limits.five_hour.used_percentage // .rate_limits.seven_day.used_percentage // "")",
  @sh "rl_reset=\(.rate_limits.five_hour.resets_at // .rate_limits.seven_day.resets_at // "")",
  @sh "rl_label=\(if .rate_limits.five_hour.used_percentage then "5h" elif .rate_limits.seven_day.used_percentage then "7d" else "" end)"
')"

printf "%b%b" "$CYN" "$model"

if [ -n "$used" ]; then
  used_int=$(printf '%.0f' "$used")
  filled=$((used_int / 5))
  bar=$(printf "%${filled}s" | tr ' ' '#')$(printf "%$((20 - filled))s" | tr ' ' '-')
  printf "%b  📊 %b[%b%s%b]%b %s%%" "$RST" "$RST" "$GRN" "$bar" "$RST" "$YLW" "$used_int"
fi

if [ -n "$rl_pct" ]; then
  rl_int=$(printf '%.0f' "$rl_pct")
  reset_time=""
  [ -n "$rl_reset" ] && reset_time=$(date -d "@${rl_reset}" +%H:%M 2>/dev/null || date -r "${rl_reset}" +%H:%M)
  printf "  %b⏱️%s:%s%%%b" "$MAG" "$rl_label" "$rl_int" "$RST"
  [ -n "$reset_time" ] && printf "  %s" "$reset_time"
fi

printf "%b" "$RST"

What it shows

The script parses the JSON that Claude Code pipes in and displays up to three pieces of information on a single line:

  1. Model name in bold cyan (e.g. "Opus" or "Sonnet")

  2. Context window usage as a 20-character progress bar (# for filled, - for empty) with a percentage

  3. Rate limit usage for the 5-hour or 7-day window, including the reset time -- this only appears when the data is available (Claude Pro/Max subscriptions)

Before the first API response, only the model name is shown because context and rate limit fields are still null.

statusline

How it works

Claude Code runs the configured command after each assistant message and on permission mode changes. The full session state is sent as JSON to stdin -- the documentation lists all available fields. The script uses jq to extract what it needs and printf with ANSI escape codes to produce colored output.

One thing to note: the // empty fallback in jq is important. Fields like used_percentage and the rate limit data are null before the first API call, so without the fallback the script would print "null" in the status bar.

The /statusline slash command can generate a script from a natural language description, but I used Claude Code itself to iterate on the script until I was happy with the output.

Claude Code tmux window names

When running multiple Claude Code sessions in tmux, every window ends up named "claude" -- making it impossible to tell them apart.

before

Using Claude Code's hooks, the tmux window can be renamed automatically based on the working directory.

Using Hooks

A SessionStart hook runs once when a session begins or is resumed:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "[ -n \"$TMUX\" ] && tmux rename-window \"cc:$(basename \"$PWD\")\""
          }
        ]
      }
    ]
  }
}

This renames the tmux window to cc:<project-dir>, e.g. cc:madflex.de. The [ -n "$TMUX" ] guard makes sure it does nothing when running outside tmux.

Since tmux rename-window disables automatic renaming for that window, the name would stick after Claude exits. A SessionEnd hook restores it:

{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "[ -n \"$TMUX\" ] && tmux set-option -w automatic-rename on"
          }
        ]
      }
    ]
  }
}

Add both hooks to ~/.claude/settings.json and use /hooks in a session to verify they are loaded.

Alternative: zsh preexec

The hooks approach is Claude Code-specific. A zsh preexec / precmd pair works for any long-running command -- claude, uv run, docker compose, etc:

# auto-rename tmux windows for long-running commands
preexec() {
  if [[ -n "$TMUX" ]]; then
    case "$1" in
      *claude*)          tmux rename-window "cc:$(basename "$PWD")" ;;
      *uv\ run\ *)       tmux rename-window "uv:$(basename "${${1##*uv run }%% *}")" ;;
      *docker\ compose*) tmux rename-window "dc:${1##* }" ;;
    esac
  fi
}
precmd() {
  [[ -n "$TMUX" ]] && tmux set-option -w automatic-rename on
}

preexec runs before each command, precmd runs before each new prompt -- so the window name resets automatically when the command finishes. The * prefix in each pattern handles commands with leading environment variables like FOO=bar docker compose up.

For Claude Code, the project directory is the most useful identifier since sessions don't have a single command. For uv run, the script name is extracted as the first argument after uv run. For docker compose, ${1##* } grabs the last word of the command -- typically the service name, e.g. docker compose up container-name becomes dc:container-name. This won't be perfect if flags come after the service name, but it works well enough in practice.

The case statement is easy to extend with more patterns.

After the change:

after

I removed the hooks from the ~/.claude/settings.json and chose to use only the zsh solution. But without investigating Claude Code's hooks I would not have found out about them!