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!

Claude Code desktop notifications

When using Claude Code in a terminal, it's easy to switch to another workspace and forget that Claude is waiting for input. Using Claude Code's hooks and notify-send, any freedesktop-compatible notification daemon (dunst, mako, swaync, fnott, ...) can notify you.

The Notification hook supports matchers for different event types. Using two Notification hooks with separate matchers covers two cases:

  • idle_prompt — Claude finished and is waiting at the main prompt (fires roughly after 60 seconds of inactivity)

  • permission_prompt — Claude needs permission to run a tool (fires immediately, which may be too frequent if you're already at the terminal)

Add them to ~/.claude/settings.json:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send -u normal -a 'Claude Code' 'Claude Code idle' \"Waiting in $(basename \"$PWD\")\""
          }
        ]
      },
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send -u normal -a 'Claude Code' 'Claude needs permission' \"Approve action in $(basename \"$PWD\")\""
          }
        ]
      }
    ]
  }
}

Both use "normal" priority, so that the notification vanishes after a little while without the need for me to click anything.

Restart Claude Code sessions after changing the hook config. Use /hooks in a session to verify the hooks are loaded.

Example for idle message:

idle

There are more hooks, but I decided to start with only Notification.

Limitations

This does not notify when Claude asks an interactive question mid-response (AskUserQuestion). There is no hook for this yet — see this feature request.

Download Historic Charges from Renault API

I already track the money I pay for charging via hledger. But I wanted a way to get the exact date when charging and a second way to get the kWh charged. For my (electric Renault) car there is an API. I already use this API in Homeassistant to get data from the car and track its position. The library used in Homeassistant is renault-api. And this library supports fetching the charging history too.

A small script gives me all charges of the car to correlate with my ledger data:

import asyncio, json, aiohttp
from datetime import datetime, timedelta
from renault_api.renault_client import RenaultClient

EMAIL = "your@email.com"
PASSWORD = "your-password"

async def main():
    async with aiohttp.ClientSession() as s:
        client = RenaultClient(websession=s, locale="fr_FR")
        await client.session.login(EMAIL, PASSWORD)
        account = (await client.get_api_accounts())[0]
        vin = (await account.get_vehicles()).vehicleLinks[0].vin
        vehicle = await account.get_api_vehicle(vin)
        charges = await vehicle.get_charges(
            start=datetime.now() - timedelta(days=365), end=datetime.now()
        )
        print(json.dumps(charges.raw_data, indent=2, default=str))

asyncio.run(main())

The Python script downloads the full history of charges from the last 365 days for the first car in the Renault account.

One dataset looks like this:

{
  "chargeStartDate": "2025-12-30T08:05:11Z",
  "chargeEndDate": "2025-12-30T08:43:57Z",
  "chargeEndStatus": "ok",
  "chargeStartBatteryLevel": 14,
  "chargeEndBatteryLevel": 81,
  "chargeEnergyRecovered": 34.800003,
  "chargeDuration": 38
},

Not only the recovered energy (kWh) and the time needed to charge, but most important the exact time of charge. My not so exact date of ledger transactions could be back dated to the correct date with this.

For the example above my ledger entry is this one:

2026-01-05 Ionity Charge
    Expenses:Car:Ionity:Charge39              €12.60
    Assets:Girokonto

So €12.60 with €0.39/kWh is 32.3kWh. Close but not identical. So there are some tolerances and probably transfer losses (for the kWh) involved.

This is a good second data source to correlate my finance data against. I will probably not run this automatically, but once I need the data I will update the already downloaded json file.