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.

Claude Skill to get Stuttgart Waste Dates

Last year I experimented with MCPs by building one for waste removal dates in Stuttgart. I used this MCP in different settings since then to demonstrate what MCPs can do and I tried to use it to experiment with different services. But overall I am actually using a Python script that pushes the next waste removal dates to my Homeassistant.

Still I wanted to see how would this MCP look like as a Claude Skill.

This skill is saved in ~/.claude/skills/stuttgart-waste/SKILL.md. And next to it the csv with all the streets of Stuttgart from the Github Repository of the str-ical2json MCP.

The SKILL.md I ended up with, looks like this:

---
allowed-tools:
  - Read(~/.claude/skills/stuttgart-waste/stuttgart_streets.csv)
  - Grep(path:~/.claude/skills/stuttgart-waste/stuttgart_streets.csv)
  - WebFetch(domain:service.stuttgart.de)

---

# Stuttgart Waste Collection Dates

1. Extract **street name** and **house number** from the user's query. Ask if missing.

2. Validate against `stuttgart_streets.csv` (in this skill's directory). Columns: `street_name`, `house_count`, `numbers`, `type`.
   - Normalize "straße"/"strasse" to "str." for matching.
   - Fuzzy-match if no exact hit. Verify house number exists in `numbers`.

3. Fetch iCal via WebFetch:
   - URL: `https://service.stuttgart.de/lhs-services/aws/api/ical?street={street}&streetnr={streetnr}`
   - Use the street name as it appears in the CSV, URL-encoded.
   - Prompt: "Extract every VEVENT. For each, list DTSTART date (YYYY-MM-DD) and SUMMARY. Format: DATE | SUMMARY. List ALL events."

4. Show only **future dates**, grouped by waste type. **Default: next 1-2 dates per type.** Full list only if explicitly requested.

The permissions are allowing to read and grep the one csv file. And the skill is only allowed to call the service.stuttgart.de domain to retrieve the ical data.

Finally one example call within an empty Claude Code shell:

❯ /stuttgart-waste for Katzenbachstrasse 58

● Searched for 1 pattern, read 1 file (ctrl+o to expand)

● Fetch(https://service.stuttgart.de/lhs-services/aws/api/ical?street=Katzenbachstr.&streetnr=58)
  ⎿  Received 33.8KB (200 OK)

● Katzenbachstr. 58 — next pickups:

  ┌─────────────┬───────────────┬───────────────┐
  │    Type     │     Next      │  After that   │
  ├─────────────┼───────────────┼───────────────┤
  │ Restmüll    │ Thu, 5 March  │ Thu, 19 March │
  ├─────────────┼───────────────┼───────────────┤
  │ Biomüll     │ Thu, 5 March  │ Thu, 12 March │
  ├─────────────┼───────────────┼───────────────┤
  │ Altpapier   │ Wed, 11 March │ Tue, 31 March │
  ├─────────────┼───────────────┼───────────────┤
  │ Gelber Sack │ Fri, 20 March │ Mon, 13 April │
  └─────────────┴───────────────┴───────────────┘

The clear downside of this setup is, that you need to use Claude Code to get the answer. On the positive side: It is impressive with how little instructions this just works.

I started to build more skills. For example one that fetches screenshots from my pastebin and reads it from /tmp. That skill is mainly to allow the exact permissions to get the image without asking me.