15 Oct 2026Over the years, I made myself a long list of CLI helpers. Stuff like git-worktree-create or video-volume-increase. They help me get the info I want or do the action I need without having to remember the exact underlying command and args to type.
But when I ask Claude to create a branch or get me the dimensions of a video, it would use bare git or ffmpeg commands. That inconsistency buggued me at first but I thought that given the agent was smarter than I was, it didn't need an alias, it could just remember the complete and complex command so it didn't really matter.
Well, it mattered, actually
When I ask the agent for a new worktree, what I have in mind is running my custom git-worktree-create helper. I packed a lot of complexity away behind that command:
- It stops early when the current directory is not a git repository.
- It starts a new branch.
- It creates the worktree folder in a shared directory, with a naming convention.
- It handles git submodules.
- It installs dependencies.
- It handles edge cases (invalid chars in branch names, etc.).
- It also takes care of some edge cases or additional tweaks for some specific repos.
This command started as a simple git worktree add /worktree/dir branch-name wrapper but grew over time to the complex beast it is today. And its tests grew with it. That command does exactly what I want it to do, and I have tests to prove it now. I feel safe using it, knowing it will do exactly what I expect it to do.
Letting the agent know
I'd like the agent to use that helper. But the agent doesn't know about it. It still thinks "Creating a worktree" is running git worktree add /worktree/dir branch-name and that's it.
I found myself having to tell it: "Use the git-worktree-create helper instead." often. And I had to do that for all my helpers.
I started adding lines into my CLAUDE.md to tell it about my favorite helpers but I quickly realized it wouldn't scale with the 1000+ helpers I currently have.
What I did instead was writing two other helpers. helper-list would list all my helpers and helper-documentation would explain how to use on.
helper-list
helper-list takes a query and returns only the helpers that match that query.

All my helpers follow a consistent naming scheme of <domain>-<object>-<action>. For example git-branch-create, docker-image-list, notion-page-read, etc.
Finding them is a matter of piping find into fzf to get some fuzzy-matching against the query and displaying them. As my helpers have explicit enough names, the agent can guess which one would be useful to it.
helper-documentation
Knowing that a helper exists is only the first step. Now the agent needs to know how to use it. helper-documentation takes a helper name as input, and returns its header documentation comments (including description, usage, etc).

By making sure all my helpers are properly documented, documentation usage can easily be resurfaced to the agent.
CLAUDE.md
To wrap this all together, here is what I added to my CLAUDE.md:
Prefer dedicated CLI helpers over raw commands.
Examples:
| Instead of… | Use… |
|--------------------------|------------------------------|
| `git` complex pipelines | `git-*` helpers |
| `convert`, `imagemagick` | `img-*` helpers |
| `ffmpeg` pipelines | `audio-*`, `video-*` helpers |
Discover helpers with `helper-list <domain> [action]`.
Read the flags and usage of a helper with `helper-documentation <name>`.
The result
Now, I can just focus on adding more helpers and more helpers, following my naming convention and documenting them properly. I'm assured my agent will be able to use them without me having to update anything in my CLAUDE.md. Those helpers are useful to both me and the agent.
A helper that finds my other helpers feels a bit meta, but it works really well.
One more benefit is that I can now configure Claude to allow specific commands (like git-worktree-create) without needing to give it a blanket permission to the whole git command.
13 Oct 2026Yesterday, my Ubuntu laptop ran an update of packages, and once it was done, it let me know I needed to reboot my laptop for the change to be completed. But I didn't want to reboot my laptop right now. I had 12+ open tabs in Kitty, each split into subwindows because I was working on various topics.
So I postponed the reboot. And postponed; for days. Finally I gave up, wrote down the list of tabs, rebooted, and manually restarted all my kitty tabs.

Doing it by hand
It was tedious, and I didn't really like doing it. And I certainly didn't want to have to do it twice. I wanted a keybinding to do it for me. Something that would save the whole layout, and bring it back.
Since Kitty 0.43, the save_as_session action dumps tabs, tab names, splits, layout geometry, and working directories to a file. And kitty --session <file> starts from that file.
I updated my Kitty to the right version and created a kitty-save script that dumps the session to a file, and bound it to Alt-F5. I also made it flash the screen green when I save (so I have some visual feedback something actually happened).
# kitty.conf
allow_remote_control socket-only
listen_on unix:/tmp/kitty-socket
map alt+f5 launch --type=background kitty-save
#!/usr/bin/env zsh
# kitty-save
sessionFile="$HOME/.local/state/kitty/session.kitty-session"
# Talk to kitty through its socket
kitty-remote() { kitty @ --to unix:/tmp/kitty-socket "$@" }
# Flash all windows green
kitty-remote set-colors --all background='#1a3a1a'
# Dump the session to a file
kitty-remote action save_as_session --save-only "$sessionFile"
# Back to the normal colors
kitty-remote set-colors --all --reset
I also created a kitty-restore script that checks for the existence of the session file, and appends --session with it if the file exists. I call that script now instead of opening Kitty directly.
#!/usr/bin/env zsh
# kitty-restore
sessionFile="$HOME/.local/state/kitty/session.kitty-session"
sessionArgs=()
[[ -f "$sessionFile" ]] && sessionArgs+=(--session "$sessionFile")
kitty "${sessionArgs[@]}"
I was happy. On the next reboot, I could just Alt+F5 to save everything, reboot, and come back where I left off.
The crash
But the next time my computer rebooted, it was because it crashed. Totally unrelated, but the computer froze and I had to manually reboot it, without being able to press Alt-F5 first.
When I restarted Kitty, it came back with the layout from my last manual save, which was days old. My tabs were not the ones I had been working on.
So, manual saving was a good start, but wasn't enough. I needed something more automated.
Every two minutes
I thought about running kitty-save automatically every two minutes. But in my mind it meant writing cron files, with their weird syntax made of * and / and that seemed too complex to me at that time.
Then I remembered we live in an age of agents and I wouldn't be the one writing those commands anyway, so why not ask the agent? Actually the agent told me that cron was outdated and systemd was apparently the new way of doing it.
A systemd user timer is made of two small files: a service to say what to run, and a timer to say when.
# kitty-save.service
[Service]
Type=oneshot
ExecStart=/path/to/kitty-save
# kitty-save.timer
[Timer]
OnStartupSec=2min
OnUnitActiveSec=2min
[Install]
WantedBy=timers.target
OnStartupSec tells it to start its first tick 2mn after login. OnUnitActiveSec tells it to run again after that every 2mn.
I had to slightly update kitty-save to work both when called manually and through the timer. I made the window green flash optional behind a --flash flag, and added some guards to return early if kitty wasn't even running.
Peace of mind
I now have this automated save running in the background every 2mn. It takes about 90ms to run, but as it runs asynchronously in the background, I don't even perceive it. And now a crash would cost me 2mn of Kitty layout at most.
04 Mar 2025
I'm using Airtable Interfaces a lot, to pilot my automations. For a project at work I built a whole pipeline to help us organize meetups in our office. It starts with the URL of a meetup.com event, and ends with the printing of the signs to put in the building, and goes through all the phases of calculating the number of pizzas needed, booking rooms in the corporate calendar, printing badges for attendees and pre-generating internal Slack messages, etc.

But the process is made of multiple steps, that having all the info on one page was getting overwhelming and very practical, so I started splitting it into several pages. One page would handle the attendee list, another would take care of the calendar, another of the Slack messages, etc.

The issue I ran into though, was that whenever I would switch pages, I would lose the context of which meetup I'm currently editing. For example I would start on the "Metadata" page for the upcoming React Meetup; I would then click on the "Calendar Events" page and would somehow now be editing the upcoming Algolia Search Party.
This is because, from Airtable Interfaces point of view, each page is an independent context, and there is no default way to keep the currently selected Event from one page to the next.
My hackish workaround


The (very hackish and imperfect) way I found around this is to add a isPinnedInUI checkbox field to my table. Only one element of the table should be marked as pinned at any given time. I also configured the record picker to select the fields where isPinnedInUi is true in priority.
I also added a custom Make scenario (trigger when clicking on the Pin blue button next to the record picker) that would uncheck the previously checked element, and check the currently selected one.
That way, when I'm working on an event I can pin it in place, and keep its context even when navigating from page to page.
Of course, this is just a bandaid, and fails as soon as more than one person is working on the dashboard at the same time. If I start pinning a meetup and my coworker Lucas is working on another one and pin the other, we'll just step on each other's toes. But so far, it has been an improvement over completely losing the context.
21 Feb 2025
I'm often taking audio notes with my phone recorder app. I then have a make.com workflow in place where I push the audio file through Whisper to get a transcript of it, and start other workflows based on the content of the transcript.
Sometimes it's just a simple note to remind me to do something, which gets added to my TODO list. Sometimes it's an idea about a blogpost I'd like to write, which gets added to my list of blogposts drafts. Sometimes it's the recap of the latest TTRPG game we played, which gets added to our campaign document.
Most of those recordings are only a few minutes long, but the last kind (the TTRPG session recap) can get lengthy, and I've already ended up with files of up to 50MB. Whisper has a limit of 25MB, so I had to come up with a way to decrease the filesize before sending it to Whisper.

I now have two paths in my make.com scenario. The bottom one is the happy path, when the filesize is below 25MB. The top one is only triggered when the file is too heavy.
In that case, I push the file through CloudConvert, to get a lighter version, and then I go to the second branch with that compressed file instead of the default one.
Compression settings
Whisper doesn't need very high audio quality to create a transcript, so I can compress pretty aggressively and still get a good transcript. What I found to be impactful is to:
- Format: Convert from
mp3 to m4a with the AAC codec. Slightly lower filesize for equal quality. - Channels: I set the channels to 1, actually making it a mono rather than stereo file.
- Audio bitrate: I halved it down from
128 kbps to 64 kbps. - Sample rate: I also halved it from
44.1 kHz to 22.05 kHz
In CloudConvert parlance, this means setting conversion and engine specific options as:
audio_codec set to aacchannels set to 1audio_bitrate set to 64sample_bitrate set to 22050
I'm not doing this conversion on every file, only on those that are too heavy. CloudConvert free plan allows for up to 10 conversions per day, which is enough for my needs. The conversion takes a few minutes to run, and again, that's fine for my current needs.
The final quality of the audio is clearly inferior to the original, but this doesn't seem to bother Whisper at all, which can still create a good transcript out of it.
Hope that help other people get over the 25MB limit of Whisper.
23 Jan 2025
In my recent no-code plunge, I discovered templated.io. It's an online service to generate images based on templates you create yourself. The first use-case I had for it turned out to be a bit too complex for what it offers, but I've now used it for something where it shines much more.
You might have stumbled upon this post through the embedded link on Bluesky, and the image for the link has been generated by templated.io.

templated.io basically gives you a simple template editor, where you drag and drop shapes, text elements and images. You assign a unique name to each of those element, and then you only have to call the API with an object of key/values to update the content of those shapes.

I used one of the default templates, adapted to my need by changing the default colors and putting my name and face. For the title, image and date, I used placeholder values (taken from a previous blogpost, to see what it would look like), and named the layers with expressive names.

Now, all I had to do was call the API with an object with what I wanted to update:
[
{
"format": "png",
"layers": [
{
"text": "Generating Bluesky Embed with templated.io",
"layer_name": "title"
},
{
"image_url": "https://v5.airtableusercontent.com/v3/u/37/37/1737655200000/7NVVJoNpTRdQFwaUSRKRyw/Ej4K-pKgYMhRgTHRyE2fvR-wnxZi_F-0LsmVTkHLJf1dpH1bcuuU3RVq3msHLNdyhdXKIVnewk9JXhhbpaiKiJc1uPt0YNYx6XyE8xH6UDMd02NJsEymlaYZUQlr6_L7H89PporK28x_HAcpqEdWSg/vJC1yCgppLMCByrKcLkJ8yMV60DIbAsMzNUuX9wMC4M",
"layer_name": "header"
},
{
"layer_name": "date"
},
{
"text": "8mn",
"layer_name": "readingTime"
}
],
"template": "857483bf-d4e3-4cf1-a32e-83d85d379314",
"transparent": false
}
]
I had a wow effect very rapidly on this tool, as it "just works™" with a very simple interface to use, and they even quickly show you the API calls to do. It's really built with the API in mind.
In the past I had tried to do the same thing with the Google Slide API, but the API is at least 10x more complex to use. I even considered using tools like Figma (whole new level of complexity) and Canva (that doesn't even have an API). Here, it took me about 5mn to have something working.
The first time I used the app, a sales person reached out to me through the integrated chat. While I don't usually like this kind of sales intrusion, the conversation was cordial, and I could see that they had really taken the time to check my blog (based on my email address) and offered assistance without being pushy. It was very refreshing from the usual sales interaction I had with other providers.
After a bit of discussion, I even realized the "sales person" I was talking to was actually the sole developer of the whole app, doing everything. That gave me even more respect for the work put in place here.
So, in a nutshell, templated.io is simple and effective, it does exactly what it's supposed to do and does it well.