My name is Olek. I write what matters to me.

Hi πŸ‘‹πŸΌ I'm Olek, an AI Product Manager. Here I write about what matters to me. On the side I build GribGrab and MarrowMinded. I'm also a co-founder of 2 kids πŸ‘¨β€πŸ‘©, married happily πŸ’, and πŸ• lover.

Visit Website β†’

πŸ“‘ Subscribe to this RSS Feed

This is an RSS feed. Copy the URL below or click a reader button to subscribe and get new posts automatically.

New to RSS? Learn more about RSS feeds

https://olekwrites.com/rss

Recent Posts

Three Widgets I Built for the Vivaldi Dashboard

Pixel art of a retro browser start page with three small widget cards on it β€” a quote card, a number sign and a weather chart

Three Widgets I Built for the Vivaldi Dashboard

Vivaldi's Dashboard takes Webpage widgets: a small web view pointing at any URL. No store, no install, no manifest, no review queue. If you can paste a link, you can add a widget.

That's a low enough bar that I built three, out of things I already run.

How to add one

  1. Open a new tab to get the Start Page, then switch to Dashboard.
  2. Click Add Widget at the bottom.
  3. Pick Webpage.
  4. Paste the URL and click Done.

Two options worth knowing. Tick Share Theme Colors while adding, and the widget picks up your browser theme (Vivaldi 7.1 and up). And each widget has two sizes β€” Regular and Tall β€” switched from the β‹― menu on the widget itself. All three below work at either size.

1. A Seth Godin quote a day

https://olekwrites.com/marrowminded/widgets/vivaldi/

84 quotes from Seth's Blog, one per calendar day. The day picks the quote, so it's the same line for everyone and it turns over at midnight β€” no draw, no repeats until all 84 are used.

Each quote shows the title of the post it came from and links to the original. The quotes are baked into the page, so it makes no network call after loading and stores nothing. It came out of MarrowMinded, my reading app.

2. Days without missing a commit

https://olekwrites.com/lenny/YOUR-GITHUB-USERNAME/widget/vivaldi

My GitHub commit streak on the Simpsons "days without an accident" sign. Swap in your own username. Add ?from=2026-07-08 to count from a start date instead of your whole history β€” useful if you're running a 100 days of code streak.

It's the same badge as olekwrites.com/lenny, which you can also drop into a README.

3. GribGrab weather

https://gribgrab.com/widget

At Regular size: your location, current temperature, the sky icon and condition, and a row with feels-like, wind and precipitation. At Tall: all of that plus a scrolling hourly meteogram for the next 7 days β€” temperature, rain, pressure, wind, cloud cover.

The gear button opens location, units and language. Units and language are shared with the app, so you set them once. If you already use GribGrab, the widget reads the location you saved there and there's nothing to configure.

One caveat: widgets can't show permission prompts, so "use my location" only works if you already granted location to gribgrab.com in a normal tab.

What building them taught me

  • Widgets are not kept alive. Vivaldi closes the page when the Dashboard closes and reloads it on every open. Cold start is the whole experience β€” paint from cache first, fetch after.
  • The box is 394px wide and roughly 155px or 380px tall. Design for both, and never let the page scroll.
  • No dialogs at all. alert, confirm and permission prompts are disabled. Anything that needs a decision has to be an in-page overlay.
  • Theme variables only exist if the user ticks the box. Every var(--colorBg) needs a fallback, or it looks broken for half your users.

Three URLs, three widgets, no install step. That part I like.

Happy dashboarding!

Read Full Post β†’

Check That Cheaper One

Pixel art of a robot with a magnifying glass searching one small shop shelf while a street of other shops stands unvisited behind it

Check That Cheaper One

More from You Can Do Better β€” small product decisions that cancel out what the product is trying to do.

I was about to buy a keyboard. I asked Claude to check the offer first.

Claude's answer: 249 zΕ‚ is 10 zΕ‚ above Keyspace and 20 zΕ‚ above another Allegro listing for the same model at 229 zΕ‚ with delivery tomorrow, so check the cheaper one; the 3/5 rating comes from a single review

It caught two things I hadn't. The price is 20 zΕ‚ above the same model sold elsewhere on the same site. And the 3-out-of-5 rating comes from one review, which means nothing.

Good answer. I saved money.

Then I read the last line again: "sprawdΕΊ tΔ™ taΕ„szΔ…" β€” check that cheaper one.

It only looked where it was standing

Every price came from the page I gave it. Another listing on the same site. A shop that listing mentioned. It found the lowest number in that set and called it the cheapest.

That's the cheapest here.

The keyboard is a standard model with a name, and a dozen other shops sell it. None of them were in the answer, because none of them were on the page. So "buy this one for 229 zΕ‚" rests on a sample of three, picked by whoever built the page I happened to open.

The second half of the job

The first half is reading the page. It did that well.

The second half is leaving it: take the model name, search the open web, come back with prices from shops the listing has no reason to mention.

Instead I got the to-do. And I did it β€” opened the listings, compared prices, checked delivery. Ten minutes of the work the answer had just proven was necessary.

What to do instead

  • Treat the page as a starting point, not the market. One site's cheapest offer isn't the cheapest offer.
  • Search on the identifier. Model name, ISBN, part number β€” that's the key to every other shop.
  • Finish the thought. If you name a next action and can take it, take it, then report what you found.
  • Come back with numbers. Three shops with prices and delivery dates beats "you can probably find it cheaper".

It found the cheapest one on the page. I needed the cheapest one.

You can do better.

Read Full Post β†’

We Value Your Feedback. Do Not Reply.

Pixel art of an open mailbox with a speech bubble rising out of it, blocked by a barrier gate reading NO REPLY

We Value Your Feedback. Do Not Reply.

First in You Can Do Better β€” small product decisions that cancel out what the product is trying to do.

I got an order confirmation from GoOrder.pl. At the bottom:

Email footer: we hope our product meets your expectations, we remain at your disposal and are happy to hear any comments you have

"We remain at your disposal and are happy to hear any comments you have."

So I hit reply.

Email header showing the sender as GoOrder, no-reply at goorder.pl

[email protected].

I checked the rest of the email. No support address, no contact link. The only link is the company name, pointing at their homepage.

The gap

One line says talk to us. The other says this address doesn't listen.

Nobody decided this. Someone wrote copy that sounds available. Someone else set up the mail service, where no-reply is the default. Neither read the other's work.

That's what this series is about: nobody made a bad call, and the result is still bad.

Why it costs

An invitation you can't accept is worse than no invitation. It reads as a script.

And the person most likely to reply is the one with a problem β€” the feedback worth having, from the customer you just bounced.

What to do instead

  • Set the Reply-To. Most mail services let you send from no-reply and route replies to a real inbox. One field.
  • Or name the address. If replies can't be received, write "write to us at support@…" instead of "we're at your disposal".
  • Read the email as a whole. Copy, sender, and links come from different people. Someone has to check they agree.
  • Do the thing you just asked the customer to do. Hit reply yourself.

The fix is one config field. What they shipped promises something the software refuses to accept.

You can do better.

Read Full Post β†’

Sexy Football, Measured β€” This Time I Was (Mostly) Right

Pixel art illustration of two football fans arguing over a chart

Sexy Football, Measured β€” This Time I Was (Mostly) Right

I have a football-geeky friend. We argue about football all the time β€” this player vs that player, this team vs that team. We split the difference on almost everything, and we both use AI to back up our own version. Last time the data proved me wrong.

This round the question was: has Slot's Liverpool played sexy football?

My claim: only at the beginning, then it stopped. His claim: it has, full stop.

What is sexy football?

You can't settle an argument about a fuzzy word. So first we made "sexy" measurable. Four ingredients, scored for every league match:

  1. Chances created (non-penalty xG) β€” 35%
  2. Passes into the box area (deep completions) β€” 25%
  3. Winning the ball high (pressing intensity, PPDA) β€” 25%
  4. Goals β€” 15%

Each match gets one number: 50 is average, 15 points is one standard deviation. A "sexy spell" is when the five-match rolling average stays at 55 or higher.

The data

I wanted to use StatsBomb open data. Plot twist: it contains no Liverpool matches from Slot's time at all. The only Liverpool season in there is 2015/16. So the per-match numbers come from Understat instead β€” all 76 of Slot's league matches.

The StatsBomb season wasn't wasted, though. I used 2015/16 to check that the index works: it jumps right at Klopp's first match in October 2015. A metric that detects Klopp arriving is a metric I trust.

The verdict

Sexy Football Index for every Liverpool league match under Slot, with sexy spells highlighted

Liverpool under Slot was sexy in spells β€” four of them, about 106 days in total, and every single one in the 2024/25 title season. The peak was the 6–3 at Spurs in December 2024. In 2025/26 the rolling index never reached the sexy zone once. The season average dropped from 54.8 to 45.2.

So: I was right that it faded, and right that year two wasn't sexy. But I was wrong about "only at the beginning" β€” the sexiest stretch came mid-season, December to February. My friend was right about year one, wrong about year two.

We split the difference again. Of course we did.

Bonus round: Slot vs Klopp

Since we had the index, we compared eras on one shared scale: Klopp's first two years, Klopp's last two, and Slot.

Klopp, first 2 years (Oct 2015 – May 2017)

Sexy Football Index, Klopp's first two seasons

Klopp, last 2 years (2022/23 – 2023/24)

Sexy Football Index, Klopp's last two seasons

Slot (2024/25 – 2025/26)

Sexy Football Index, Slot's two seasons, on the shared scale

Era Mean index Days in sexy spells
Klopp, first 2 years 48.0 50
Klopp, last 2 years 53.2 202
Slot 48.6 87

Late Klopp wins, and it isn't close. Early Klopp pressed the hardest of the three but created the least. Slot creates almost as much as late Klopp but presses far less β€” control football, which the index prices as less sexy.

What I learned

  • Define the fuzzy word first. Half of our argument dissolved the moment "sexy" became four numbers with weights.
  • Check the dataset before you promise anything. The source I planned to use had no data for the question I asked.
  • A metric needs a sanity check. Validating the index on a known style change (Klopp's arrival) is what made me believe the rest.
  • Both of us were partly right. Again. Data doesn't always pick a winner β€” sometimes it just tells you which half of your claim to keep.

What's your definition of sexy football? I bet your weights would be different. Would love to hear them.

Best, Olek

Read Full Post β†’

Days Without Missing a Commit

Days without missing a commit β€” my current streak on the Lenny sign

Days Without Missing a Commit

My GitHub commit streak, on the Lenny "days without an accident" sign from The Simpsons. Paste a GitHub username at olekwrites.com/lenny and you get a live badge for your README β€” it re-renders itself from your contribution data, every day, on its own.

I built it to keep my 100 Days of Code PACT honest: the chart shows the squares, the sign shows the number.

The sign above is running ?hand=prev, which puts yesterday's number on the card in Lenny's hand: whatever the sign says today, his hand says the day before.

Read Full Post β†’

The Email My App Never Sent

Pixel art of one envelope landing in an inbox, then many envelopes fanning out to a row of inboxes

The Email My App Never Sent

More from Building MarrowMinded β€” the reading app I build with Claude Code. Start here: I built MarrowMinded.

MarrowMinded gives you one lesson a day from a book. The catch: you have to open the site to read it. I wanted it to come to me instead β€” land in my inbox each morning, no clicking.

I'd even written the send code once, months ago. It never sent a single email.

Why it never worked

The honest reason isn't technical. The app had no users β€” just me. Real delivery meant a subscriber list, a scheduler, and a mailer, and that was more work than it was worth for an audience of one. So the send code just sat there, waiting for a reason to run.

What changed wasn't motivation. It was friction. When I found n8n, delivery got cheap enough to build that I finally wired it up β€” just for myself.

The MVP: email myself

So I built the smallest version that would help me today: the lesson in one inbox β€” mine.

I built it in n8n. Three steps:

  1. A schedule that fires at 7am.
  2. An HTTP call to a small endpoint I added to the app, which returns today's lesson.
  3. An email node that sends it to me.

That's it. It's live, and it runs every morning. Not multi-user, not clever. But it does the one thing the app never did: it shows up on its own.

Why n8n and not another cron script? Because I'd been here before, and I didn't want to hand-write scheduling, an email client, and retry logic all over again. n8n gives me the schedule, the API call, and the email as three boxes I can see. When something breaks, it tells me which box.

The full version: email everyone

Sending to myself is easy. Sending to other people needs three things the MVP skips.

A list. Who wants the digest, and which book they're on. New sign-ups have to land somewhere.

A record. What went out, and what failed. Without it I'd double-send, or silently drop people when a message bounces. I need "sent" versus "failed", per person, per day, so I can retry just the failures.

A real sender. My personal Gmail is fine for one email a day. For a list I need a proper email service β€” I'm looking at Sender β€” that handles volume and deliverability.

Where the data lives

Here I have two options.

  • n8n's own data tables. Built in, nothing to set up. For a small list, this is plenty.
  • A Postgres database on Render, where the app already runs. More control, and the app can read the same data.

I'll start with n8n's tables, because there's less to wire up, and move to Postgres when the list grows or the app needs the data too.

The final flow

Then it's one n8n workflow:

Schedule β†’ read the list of subscribers β†’ for each person, fetch their lesson β†’ send it with Sender β†’ write back "sent" or "failed" with a timestamp β†’ next run, retry the ones that failed.

A small signup form on the site drops new addresses straight into the list. That's the whole thing.

Takeaways

  • Ship the version that helps you first. My own daily email was the part I could build today. The multi-user version can wait, and now it has a working base to grow from.
  • Friction decides what gets built, not just difficulty. Delivery was never hard β€” it just wasn't worth the setup for an audience of one. A tool that made it cheap changed the math, and I finally shipped it.
  • Let the tool hold the plumbing. Schedule, API call, email, retries β€” n8n gives me those as parts I can see, instead of a script I have to babysit.
  • Track what you send. For anything going to other people, "what was sent and what failed" is not optional. Design it in from the start.

What's the boring half you keep not finishing? Mine was hitting send. β†’ olekwrites.com/marrowminded

Read Full Post β†’

Which AI Writes the Best Lesson? I Made Them Grade Each Other

Pixel art of four small robots in a ring, each holding up a numbered scorecard, judging one in the middle

Which AI Writes the Best Lesson? I Made Them Grade Each Other

More from Building MarrowMinded β€” the reading app I build with Claude Code. Start here: I built MarrowMinded.

MarrowMinded gives you one lesson a day from a book. Under the hood it can run on different AI models β€” Claude, GPT-4o, Gemini, and Groq's Llama. Which one writes the best lesson? I'd been guessing. So I built something to actually find out.

The setup

Same chapter, four models, one question: whose lesson is best?

The trick is who judges. A single judge has favourites. So I rotated it: each model grades the other three, and every lesson gets scored by three different judges. Then one more pass where Claude grades all four β€” including itself β€” to check whether a model plays favourites. A small script adds it up and logs every score to a table.

I built the whole thing in n8n β€” four model nodes, the judges, the scoreboard β€” so I could see each part instead of burying it in code.

The result wasn't clear-cut β€” which is the interesting part

By average score, Claude won: 9.33. Gemini close behind at 9.0, GPT-4o 7.67, Llama 6.67.

But by head-to-head wins, Gemini took three of the four rounds. Claude scores highest on average; Gemini gets picked as the single best most often. Two honest ways to count, two different winners. A real test shows you that instead of smoothing it over.

Does Claude favour Claude?

When Claude graded all four, it picked itself. Suspicious. But two things clear it: the three other judges also ranked Claude top, and when Claude judged a group that didn't include itself, it picked Gemini. So it's not playing favourites β€” it just rated the best lesson best.

That check β€” does the judge favour its own kind? β€” is the part I'm proudest of. It's easy to crown a winner. It's harder to show the crown is fair.

Two surprises

  • Gemini's small "flash" model punched way above its weight. The cheapest and fastest of the four, and it nearly won.
  • Llama came last as a writer β€” and was the worst judge too. It sometimes returned broken output and scored the strongest lesson a 1. The test grades the judges, not just the writers.

The hard part wasn't the AI

Honestly? The models were the easy bit. The two hours of pain were billing and paperwork: one provider out of credits, another that had quietly retired the model I was calling, and a third where I'd topped up the wrong account entirely. None of it was the actual work. When you wire up four vendors, most of your time goes to their plumbing, not yours.

Takeaways

  • Don't trust one judge. Rotate it and average across several. One grader has taste; three have a signal.
  • Test the judge, not just the answer. Check whether it favours its own kind before you believe the ranking.
  • Count more than one way. Average and head-to-head disagreed here β€” both were true.
  • Budget for vendor plumbing. The AI is a box; the accounts around it are where the day goes.

If you had to bet β€” which AI writes the best lesson? Come read one and judge for yourself. β†’ olekwrites.com/marrowminded

Read Full Post β†’

Add, Then Trim

Pixel art of an app screen filling up with stacked toolbars, then a broom sweeping most of them away, retro two-panel before-and-after

Add, Then Trim

More from Building MarrowMinded β€” the reading app I build with Claude Code. Start here: I built MarrowMinded.

MarrowMinded started tiny. One screen, a lesson a day, almost nothing on it. I liked it that way.

Then I used it. And using it gave me ideas β€” a chat to ask about the book, a way to jump to any chapter, a progress bar, quicker navigation. Each one was useful. Each one earned its place.

A year later I opened it one morning and felt tired before I'd read a word. All those useful things, stacked on top of each other, had quietly turned a calm little app into a busy one.

Every feature was a yes

Each thing I added was a yes. Yes, that's handy. Yes, I'd use that. And every yes was right on its own.

The trouble is that yes-es pile up. Nothing felt like clutter the day I added it. It only added up.

So I did something I don't do often enough: I stopped adding, and spent a whole round just taking away.

Why not trim as I go?

Good question. I asked myself the same thing. If I'd cut while I built, it would never have gotten cluttered in the first place.

Here's my best answer: adding and trimming are two different modes, and I'm bad at holding both at once.

When I'm building, I'm saying yes β€” chasing the next useful thing, in love with what could be there. When I'm trimming, I'm saying no β€” cold, protective of the reader's attention, happy to delete. One mode looks forward at what could exist. The other looks at what already does and asks if it earns its keep.

Flipping between them every ten minutes is exhausting, and I'm not sure it even works. So now I don't. I build in one round and trim in another. Expand, then contract.

The trimming round

This time I didn't guess at a fix. I asked Claude to draw four minimalist designs at once and show them side by side β€” same screen, four looks. Making the options was nearly free, so instead of defending my first idea, I just compared and picked the one that stood out.

Then the cutting. Four stacked bars at the top became two. Grey boxes became open space. The tools all stayed β€” jump, chat, progress β€” I just pulled the noise out from around them. Same app, every button still there. It just stopped shouting.

One nice catch: I almost shipped it off the mockups, then ran it for real and found my own chat messages were white text on a cream bubble in dark mode. Invisible. A picture would never have shown it. Check the real thing, not the flattering screenshot.

Takeaways

  • Build in rounds. Add in one pass, trim in another. Trying to do both at once is a different job every ten minutes.
  • Every feature is a yes, and yes-es pile up. Nothing feels like clutter the day you add it. Schedule a round of no.
  • Minimalism is removing layers, not features. Keep the tools. Cut the noise around them.
  • Compare, don't guess. A few cheap options side by side beat one idea you'll only defend.

When did your side project last get a round of no? Mine was overdue. β†’ olekwrites.com/marrowminded

Read Full Post β†’

100 Days of Code, as a Tiny Experiment

My GitHub contribution chart β€” one green square for every day I show up

100 Days of Code, as a Tiny Experiment

"A PACT is a commitment to a specific action for a set period of time. Purposeful. Actionable. Continuous. Trackable." β€” Anne-Laure Le Cunff, Tiny Experiments[^1]

I've been reading Tiny Experiments, and one idea stuck.

Most of us set goals like "get good at X" or "build a habit." Then we fail, and we decide the problem is us. Le Cunff says the problem is usually the frame. Instead of a goal β€” a far-off finish line you either hit or miss β€” you make a PACT: a small, repeatable action you do every day for a set number of days, just to see what happens.

Not "become a better developer." Just: do the small thing, every day, and see what I learn.

So here's mine.

The PACT

I will commit code to my repo every day for 100 days.

Let's hold it up against the four letters:

  1. Purposeful. I want writing code β€” with Claude Code doing most of the typing β€” to be a daily reflex, not a when-I-feel-like-it thing.
  2. Actionable. I can do it today. No gear to buy, no course to finish first. Open the laptop, ship something small.
  3. Continuous. Every day. Small counts. One honest commit beats a heroic weekend and then nothing.
  4. Trackable. This is the part I love. There's already a perfect tracker built for it, and I don't have to maintain it by hand.

The tracker maintains itself

That green grid at the top of this post is my GitHub contribution chart, rendered live. Every day I commit, a square lights up. Miss a day, and there's a grey gap staring back at me.

I didn't build it. I don't update it. It just tells the truth β€” which, if you've read my post on the TODO list nobody maintains, you'll know is exactly the kind of tracking I trust: the kind that runs without me.

That's the whole appeal of a PACT. It's not about willpower. It's about making the work visible and letting the chart do the nagging.

Okay, I did end up building one thing. The grid shows the squares, but it doesn't show the number β€” so I put my streak on the Lenny "days without an accident" sign from The Simpsons:

Days without missing a commit β€” my streak since day one of the PACT, on the Lenny sign

It's a live image counting the days since day one of this PACT, and it re-renders itself from my contribution data β€” so like the chart, it nags without my help. If you want your own for a README or a blog post, grab it at olekwrites.com/lenny β€” paste your GitHub username and it hands you the embed.

Why not a goal?

Because I've set the goal "code more" a dozen times and it means nothing. There's no day where I've done it. There's no square to fill.

A PACT is different. It doesn't ask did you succeed? It asks did you show up today? β€” a question I can answer with a yes or a no, every single day. And at the end of 100 days I won't have "become a better developer" (whatever that means). I'll have 100 squares, or I won't, and either way I'll have learned something about how I actually work.

That's the experiment. Not will I win β€” but what will I find out.

The book, one lesson at a time

I didn't just read Tiny Experiments once and shelve it. I loaded it into MarrowMinded, an app I built that sends me one lesson a day from books I've already read.

So now the book itself is a daily habit feeding a daily habit. Each morning I get one idea from it. Each day I make one commit. If you want to dig into the ideas in the book, that's where I keep them β€” one small piece at a time, instead of a 300-page block I'll forget in a week.

Takeaways

  • Swap the goal for a PACT. Not "get good at X" but "do X, daily, for N days." A question you can answer every day beats a finish line you keep missing.
  • Make it trackable by something that isn't you. The best tracker is the one you never have to maintain. Mine is a chart that updates itself.
  • Small counts. One honest commit keeps the streak alive. The point is showing up, not the size of the day.
  • Run it to learn, not to win. A tiny experiment doesn't ask did you succeed β€” it asks what did you find out.

Running your own tiny experiment? I'd love to hear what your PACT is β€” and whether the chart nags you as much as mine nags me.

[^1]: Anne-Laure Le Cunff, Tiny Experiments (2025). The PACT framework comes from her work at Ness Labs.

Read Full Post β†’

The Feature That Found a Bug

Pixel art of a world map on a retro screen, a glowing crosshair over the Arctic revealing a small hidden bug icon

The Feature That Found a Bug

Part of Building GribGrab β€” a series on what I learn building a weather app with Claude Code.

GribGrab had four ways to pick a place: type a city, paste coordinates, tap GPS, or reuse a saved one. All of them are text. None let you just point.

So I added a map. Drag it around, and the forecast loads for whatever sits under the crosshair. That's the whole feature.

The interesting part came after.

I could've used a library

There's a library for this β€” Leaflet. Drop it in, done in an hour. I didn't. I wrote the map myself, about two hundred lines.

Why? GribGrab runs on one rule: no code from anyone else's server. It keeps the app light and private, and I'm the only one who can break it. The cost is real β€” I now own every pinch-zoom quirk Leaflet would have handled. But it's a cost I can see.

That's the trade with a library. It doesn't remove the work. It hides it, and hands you the bill later.

Then I tapped the Arctic

The moment the map worked, I could tap anywhere. So I tapped the North Pole. The chart went completely gray.

The app shades the night-time hours grey. Up there it had shaded the whole week.

The reason is nice. At the poles in summer the sun never sets. My weather source says so honestly: sunrise at midnight, sunset at midnight the next day β€” 24 hours apart. My old code read both as "00:00," called the whole day night, and greyed everything out.

That bug had been live for months. Nobody hit it, because reaching the Arctic meant typing its coordinates, and who does that? The bug wasn't new. The map just built a one-tap road to it.

A new feature is a flashlight

This is the part I keep relearning. You think a new feature adds a room. It doesn't β€” it adds a door, onto rooms that were always there in the dark. This one literally: the room I walked into was a chart greyed out into permanent night.

The map didn't create the polar bug. It created the first easy path to it. Same as a search box surfaces the query nobody typed.

So shipping something new is also my cheapest bug finder. Every door shows you a room you never checked.

The quiet cost

The map isn't free β€” not in privacy. A map is a grid of little numbered image tiles, and to draw your view the browser has to ask OpenStreetMap's servers for the exact tiles you're looking at. Each of those requests carries your IP address and which squares of the world you asked for. That's how they'd know β€” not from anything I send them, but from your own browser fetching the picture. Pan to your house and you've told them roughly where you are.

Fair trade for a map you can point at. But it's a trade, so it goes in the privacy policy in plain words. A feature that phones a third party always has a bill. The least I can do is print it.

Takeaways

  • Ship it, then poke the corners. Tap the ocean, the poles, the empty box β€” the edges you'd never reach the slow way.
  • A library moves the work, it doesn't delete it. Fine sometimes. Just know you're choosing where the bill lands.
  • New paths surface old bugs. Make something easy that was only possible, and expect it to walk into code nobody had run.
  • Say the privacy cost out loud. If a feature phones a third party, tell users. "Free" usually means someone else pays in data.

New doors, old rooms. Go open one. β†’ gribgrab.com

Read Full Post β†’

A Skill Is Just You, Written Down

Pixel art of an open toolbox on a retro screen, five glowing labeled cards floating out of it like tools

A Skill Is Just You, Written Down

I keep telling people: if you can describe what you want, you can build it now. I build my apps that way. I talk, Claude Code types.

This week I built something different. Not an app β€” a set of small tools for Claude Code itself. And there's no code in any of them.

They're all in one public repo: claude-code-skills.

Update Β· 4 July 2026 β€” Two more since I hit publish: translate-en-to-pl and translate-pl-to-en. They don't just translate β€” they confirm what I meant first, then hand me three versions with the register and the reason for each one spelled out. Which quietly breaks my "they're all thinking tools" line below. Fine by me: a skill turns out to be just as good at doing a thing as thinking one through. Still only me, written down.

Update Β· 5 July 2026 β€” Added clarify-doc, the one I'll use most. Give it something you've written and it checks whether a reader can actually follow it β€” same word for the same thing throughout, each sentence leading to the next, each paragraph to the next β€” then rewrites it plain. It leaves its notes as invisible comments right on the lines they're about, so the rendered page stays clean. This whole update log got a pass from it before I hit save.

What a skill is

A skill is a folder with one file inside: SKILL.md. The top says what it is and when to use it. The rest is plain instructions, like a note to a helpful colleague.

No code. No install. You drop the folder in, and Claude decides on its own when to use it.

That's the hard part. A good skill fires at the right moment and stays quiet the rest of the time. The trick isn't what it does. It's teaching the tool when to use it.

My favourite: cruciable

I wanted to test an idea before committing to it. Not a yes-man. A real argument.

So I described it, and by the afternoon it existed. You give it a claim β€” "I should do X." It runs a three-round debate: one side for, one side against. The rules: answer each other first, then add something new. No repeating. Examples welcome. At the end it hands you the strongest point on each side, and the one question it all comes down to.

Then it lets me decide.

I've used it on real decisions since. It's good to watch both sides of your own head argue it out, in order, instead of going in circles.

The other four

All thinking tools, it turns out:

  • mental-models β€” walks me through a hard choice using Naval Ravikant's frameworks, one question at a time.
  • thinking-partner β€” a ten-minute session that ends with one clear next step.
  • stoic-mentor β€” a Stoic mentor that asks instead of tells. I keep it in English and Polish.

None of them are clever. That's the point.

The twist

A skill isn't code. It's taste.

mental-models is how I like to break down a choice. stoic-mentor is the questions I want asked when I'm stuck. write-post wrote the first draft of this post.

I didn't teach Claude a task. I wrote down how I think, and handed it over.

Why share them

The most honest thing you can show isn't a polished demo. It's what you actually use on a normal Tuesday.

Sharing them cost me nothing β€” a README, a licence, one push. But these are the tools I use when no one's watching. That's building in the open: don't tell people what you can do. Hand them the thing you made.

Takeaways

  • A skill is instructions, not code. If you can write a good note, you can write one.
  • The hard part is the trigger. When it fires matters more than what it does.
  • Write down how you think. The best tools aren't clever. They're yours.
  • Share what you actually use. Real proof is the daily tool, not the demo.

Take any of them β†’ github.com/jaal/claude-code-skills

Read Full Post β†’

Now It's Apps' Turn to Get Cheap

Pixel-art price tags falling across a blog, a music note, a film reel, and a phone app

Now It's Apps' Turn to Get Cheap

Once the foundational tools are good enough, technique and training outperform hardware. β€” Seth Godin, "You don't need a better camera"

Seth wrote that about cameras this week. The gist: past a certain point, a better one won't make you better. New snow tires beat a new car for winter driving.

It got me thinking about something bigger. The tools don't just get good enough β€” they get cheap enough, over and over, until the only expensive thing left is the idea.

Take blogs. In 1995, having one was a project. A server, or money for hosting. Some HTML, maybe a web server to configure. And even then you were shouting into an empty room β€” no feed, no algorithm to find you. Real money, real knowledge, real time.

Ten years later, none of that was true. You signed up for Blogger, picked a theme, and wrote. Free. Today you can stand up a blog on your own domain in the time it takes to make coffee.

I keep noticing this pattern. The cost of making a kind of thing starts impossibly high, then collapses.

The same arc, over and over

  • Words. A blog went from servers-and-HTML to a free signup in about a decade. Now anyone can publish under their own domain in seconds, for almost nothing.
  • Music. Recording once meant a studio and a label. Then a laptop and a cheap interface made a bedroom good enough.
  • Video. Cameras, editing suites, broadcast deals β€” then YouTube and a phone in your pocket.
  • Graphics, then video again β€” this time with AI. What used to need a designer, or a film crew, now needs a prompt.

Each time, the same thing happens. The expensive parts β€” the gear, the skill, the distribution β€” fall away one by one. What's left is taste and the idea: knowing which thing is worth making, and being the one to think of it. Those don't get cheaper. When everyone can make the thing, the only edge left is picking the right thing and making it well.

And each time, a wave of people who could never have made that thing before suddenly can. With something like Claude Code, the distance from "I wish this existed" to a thing that runs is an afternoon β€” you describe it in plain words, it writes the code.

The other half: getting found

But making was only ever half of it. The other half is getting someone to look.

Before social media, that was pure word of mouth. Seth tells a story from the days before a Share button existed: people would print out his posts and leave them on a colleague's desk with a note β€” "you have to read this." They faxed them between offices. They forwarded the email to the whole team. Not to promote Seth β€” to look smart themselves. A good idea was a thing you passed along, because passing it along made you look sharp.

And he still works that way. Seth doesn't really promote the blog. He just shows up β€” every single day, for years β€” and writes something that might be useful to someone else. The showing up is the whole strategy.

Apps are next

Software was the holdout β€” two costs in one: writing the code, and running it. Both are caving now, just not in step. Running went first; cloud platforms ate most of that pain over the last decade. Writing's going now, with AI doing the typing. And this week I felt the running side fall even further.

The making I've written about before: I build my apps with Claude Code and barely touch the code by hand. What got me this week was the running. So I ran an experiment β€” moved two apps, GribGrab and MarrowMinded, from Render to Railway, just to feel how it goes now.

I didn't really do it. I told Claude Code to, and it did β€” figured out the build, the start command, wired up the database. One command, a couple of minutes, a live URL. Turbo fast.

Not all smooth. The first deploy landed on top of my database and wiped it, because the tool defaults to whatever service you touched last. One flag fixes it. Every platform has an edge like that; the trick is finding it before it finds you.

Money? Roughly a wash β€” a few dollars either way.

So price didn't decide it. Time did. The gap between "works on my laptop" and "live on the internet" used to be a chore. Now it's one prompt. And when shipping is that cheap, you ship more β€” even the small things you'd never have bothered with.

What this is really about

So it's not the tools. It's the direction. Shipping an app used to be a wall. Now it's a button β€” same as blogging, music, video, images before it. The expensive parts fall away, and you're left with the only two questions that ever mattered: do you have something worth making, and can you get anyone to see it?

Takeaways

  • Let an AI agent write the code. You keep the taste and the decisions; it handles the typing.
  • Pick a platform that ships in one command. I tried both Render and Railway and I'm happy with each β€” the point isn't which one, it's that the hop from "works locally" to "it's live" is now a single step.
  • Every tool has one sharp edge β€” find it early. Mine was Railway deploying on top of my database and wiping it. Trip over it once, write it down, move on.
  • Ship the small thing. When deploying is free and instant, build the tiny app you'd never have set up a server for. The point isn't the app. It's that you finally can.

What have you been sitting on because shipping it felt like too much? The wall's gone β€” go make something. β†’ gribgrab.com

Read Full Post β†’

I Lost My Setup Once. Now It Lives in a Repo.

A pixel-art floppy disk saving a tiny house into the cloud

I Lost My Setup Once. Now It Lives in a Repo.

This week I switched to a new laptop. Nothing dramatic β€” nothing lost or stolen. Just the old one out, a new one in.

Almost everything moved over on its own. Documents, apps, settings β€” all saved in the cloud, all there when I logged in. Everything except one folder: my Claude Code config, plus a small file that lists my projects. That stuff was never saved anywhere. So it was just gone, and now I have to set it all up again from memory.

I'd been here before. You don't notice these things until they're missing β€” the shortcuts, the small tweaks, the settings you build up over months and never write down. Then you spend days trying to remember a setup you can't quite picture.

No use crying over spilt milk. So this time I decided to do it right.

The Quiet Risk

I use Claude Code in my terminal every day. Over time it had become mine β€” custom hooks, a start-up routine, project settings, a few skills I'd written.

All of it lived in one folder on one laptop: ~/.claude.

The day I moved machines, none of it came with me. Same mistake as last time, just a different folder.

The Fix: My Config Is a Git Repo Now

The answer was obvious once I said it out loud. If my writing lives in a Git repo, why not my setup?

So I turned ~/.claude into a private repo on GitHub. But config folders are messy. They hold old history, cached files, and the odd password. I didn't want any of that going to the cloud.

The trick was to flip the usual rule. Instead of listing the files to skip, I skip everything, then add back only the safe ones:

/*
!/settings.json
!/hooks/
!/skills/

Now I can't upload my history or a password by accident. The repo can only ever hold the few files I picked.

The Part I Like: It Backs Itself Up

A backup you have to remember isn't really a backup. So I made it automatic.

Claude Code can run a small script each time it finishes a task. Mine checks if anything in the config changed. If it did, it saves and uploads the change on its own, quietly in the background:

  • Nothing changed? It does nothing.
  • Something changed? It saves it with a timestamp and uploads it to the private repo.

I don't think about it. I change a setting, keep working, and by the time I look up it's already safe in the cloud.

What This Actually Buys Me

  • A new laptop is one command away. Not days of building things back from memory.
  • A history of my own tools. I can see how my setup changed over time, and undo a change I regret.
  • No passwords in the cloud. The setup makes sure of it.

The Lesson, Again

I keep learning the same thing: the work is safe, but the setup around the work is easy to lose. Your shortcuts, your small tweaks, your little bits of automation β€” they're worth saving too.

If a tool lets you save its settings as plain files, put those files in a repo. Make the backup automatic, so you're never the one who has to remember.

Future you, staring at a blank new laptop, will be glad you did.

Back up your setup, not just your files.

Read Full Post β†’

The Analytics Were Mostly Me

Pixel art of a retro analytics dashboard with a rising line chart, and a small developer figure secretly pushing the bars up from behind

The Analytics Were Mostly Me

Part of Building GribGrab β€” a series on what I learn building a weather app with Claude Code.

I added analytics to my weather app and felt great about it. The daily-users line mostly went up. Real people, checking real weather.

Then I looked at who they actually were.

The day that gave it away

One day stood out. Lots of "active users," a nice tall bar. Then I removed my own visits β€” and nearly two-thirds of that day was one person. Me. On one device, reloading the app while testing a change to the chart.

63% of a "great" day was just me debugging.

It wasn't a one-off. Over two weeks, my own testing was padding the average by roughly 18% β€” but unevenly. Almost nothing on a quiet day; a third or more on days I was deep in the code. That's the trap: the noise is loudest on the days you work hardest, which are exactly the days you want the chart to mean something.

The numbers weren't lying. I was just standing in them.

How I found this out: PostHog + Claude Code

Here's the part I like. I didn't click around a dashboard tool to work any of this out.

I use PostHog for analytics, and it has an MCP server β€” basically a way for Claude Code to talk to PostHog directly. So I stayed in my normal coding session and just described what I wanted, in plain English, and Claude Code did it for me:

  • "Build me a dashboard for active users and retention." β†’ it created the dashboard.
  • "Break daily users down by person and show me who loads the most." β†’ it ran the queries and showed me the data.
  • "That top user is me. Add a filter that excludes my traffic from every chart." β†’ it built the exclusion and applied it everywhere.

No tab-switching, no learning a query language, no hunting through menus. I talked to my data like a coworker. Finding myself in the numbers, and then taking myself out, was a five-minute conversation in the terminal.

Why you're worse than bots

Everyone knows to filter out bots, and analytics tools do it for you. But you β€” the person building the thing β€” are the heaviest user by far in the early days, and nothing flags you.

You load the app more than anyone. You reload after every deploy. You open it on your phone to check the mobile view. You leave a tab running that refreshes on its own. Every one of those gets counted. To the dashboard, you look like your most loyal fan.

One of my "users" had loaded a city 125 times in a single week from one device. That's not a superfan. That's me.

When you only have a handful of real users, one busy developer doesn't bend the numbers. He is the numbers.

I thought I'd fixed this on day one

I wasn't careless about it. From the very start I added a secret switch: open the site with ?internal in the address and it would turn analytics off for me.

It didn't work the way I assumed. It muted exactly one page β€” the one I landed on. Click through to another city, or come back the next day, and it had already forgotten. It started counting me again the moment I did anything.

So for weeks I believed I was excluded, while I was quietly polluting my own data the whole time. A fix that half-works is worse than no fix, because it stops you from checking.

Finding myself for real

There's no "this is the founder" flag, so I had to teach the system to recognise my own footprints. Three gave me away:

My own subdomain. I'd set up a separate web address β€” a sandbox β€” just for my own testing, so I can try things before they go live. That turned out to be the cleanest signal of all: anything coming from that address is me, full stop. No real user ever goes there.

My city and browser. My laptop shows up in my home city, on a browser almost nobody else in my numbers uses. That combination is basically always me.

The version I first showed up on. I was the only person using the app in its earliest builds β€” before I'd shown it to a single other person. So "first seen on a pre-launch version" is a clean tag for my own devices, including my phone, which the first two signals missed.

I handed those three rules to Claude Code, it turned them into an "internal traffic" group in PostHog, and pulled that group out of every chart. The growth lines barely moved. But the engagement lines β€” my "power users," my regulars β€” shrank hard once I was gone. That's the part that stung.

The real fix: don't record it at all

A filter cleans up the dashboard, but the junk is still in the database, and I'd have to keep that filter current forever. Better to never collect it.

So now the app simply refuses to count me when it's obviously me:

// Don't track my own testing.
if (
  location.hostname === 'localhost' ||
  location.host.includes('sandbox') ||
  localStorage.getItem('internal') === '1'
) {
  analytics.optOut();
}

My own computer and my sandbox subdomain opt out on their own. And for the live site, I visit once with ?internal β€” but this time the choice is remembered on that device, for good, instead of lasting a single page.

Two layers: the filter cleans up the past, the opt-out stops me being recorded in the future. One is mopping; the other is not spilling.

What changed

My dashboard finally shows strangers. I keep two versions of the users line side by side β€” with me and without me β€” so the gap between them is the first thing I see. When the two drift apart, I know I've just been busy, not popular.

The uncomfortable lesson: early analytics flatter you by default, because the person reading the numbers is the same person making most of them. You have to deliberately take yourself out to see anything true. Until you do, you're not measuring your product. You're measuring your own enthusiasm.

Takeaways

  • In a young project, you β€” not bots β€” are the biggest source of fake data. Remove yourself before you trust a single number.
  • Your traffic spikes on the days you work hardest, exactly the days you want to read as growth.
  • A half-working fix is dangerous. My day-one ?internal switch only muted one page, so I felt safe while polluting everything. Make sure your fix holds across a whole session, not one click.
  • Give yourself an obvious tell. A separate subdomain for your own testing makes "is this me?" trivial to answer.
  • Two layers beat one: a filter to clean the history, an opt-out to stop recording yourself going forward.
  • If your tools have an MCP server, use it. I built the dashboard, found myself, and applied the filter by talking to PostHog through Claude Code β€” no UI, just plain English.

How much of your product's "engagement" is really just you, hitting refresh? I didn't know until I subtracted myself β€” and most of a good day disappeared. β†’ gribgrab.com

Read Full Post β†’

The TODO List Nobody Maintains

Pixel art of a checklist on a retro screen, items auto-checking themselves with a small robot hand

The TODO List Nobody Maintains

Part of Building GribGrab β€” a series on what I learn building a weather app with Claude Code.

While building my weather app, I did the thing everyone does: I started a TODO.md.

It was great for a week. Then I stopped using it.

Three reasons it stopped working

1. My new ideas were better than the old ones. Every time I sat down to work, I had a fresh idea. It was usually more interesting than anything already on the list. So I'd chase the new thing, and the old items just sat there getting older.

2. The list never showed up. It was a file in a folder. To see it, I had to remember it existed and go open it. I never did. Out of sight, out of mind β€” so when I started working, the list wasn't in my head at all.

3. Keeping it updated took time. Checking off done items, adding new ones, cleaning out stale ones β€” that's work. Work with no payoff. So I skipped it, and after a while the list didn't match reality anymore.

Once a list stops matching reality, you stop trusting it. Once you stop trusting it, it's dead.

The fix: let the assistant do it

I work inside Claude Code all day. It already has everything it needs to keep the list honest: my commits (what I actually did), my TODO file (what I said I'd do), and today's date (how long each item has been sitting there). Keeping the list honest is just comparing those β€” including each item's age against the clock.

That's a chore, not a decision. So I gave it away. No new habit, no discipline. I tied it to two moments that happen on their own anyway: opening the project and committing code.

Here's how each of the three problems got solved.

Problem 2 first: make the list show up

This was the big one. I made the list appear every single time I open the project β€” before I do anything else.

A hook in .claude/settings.local.json hands Claude the list, today's date, and my recent commits, and asks it to brief me:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Compare TODO.md against the recent commits, then tell me: what got done, what is stale (open more than 14 days, or marked ⏳), what is missing, and what to archive (done more than 30 days ago).' && echo '--- today ---' && date +%Y-%m-%d && echo '--- TODO.md ---' && cat TODO.md && echo '--- last 15 commits ---' && git log -15 --pretty=format:'%ad %s' --date=short"
          }
        ]
      }
    ]
  }
}

Now I can't miss it. Every session opens with: here's what shipped, here's the thing you've been ignoring for three weeks, here's what you meant to do and never wrote down. The list is the first thing in my head, not a file I forget.

Problem 3: stop spending time on it

The same hook does the cleanup. Claude moves finished items to Done, stamps the dates, and sweeps anything done more than 30 days ago into an archive file. I don't touch it.

A second hook keeps it current as I go. After every commit, it checks whether the commit finished anything on the list and updates it:

{
  "PostToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "python3 -c \"import sys,json; d=json.load(sys.stdin); cmd=(d.get('tool_input') or {}).get('command','') or ''; print(json.dumps({'hookSpecificOutput':{'hookEventName':'PostToolUse','additionalContext':'A git commit just landed. If it finished a TODO.md item, move it to Done (done: today), add any follow-ups (added: today), and flag stale items. Do nothing if none of this applies.'}})) if 'git commit' in cmd else None\""
        }
      ]
    }
  ]
}

It runs after every command but stays silent unless the command was a git commit. Maintenance cost: basically zero.

Problem 1: catch the good new ideas

The new ideas were the whole reason I kept ignoring the list β€” so now I make sure they get caught instead of lost.

At my desk, a fresh idea goes straight into the list, stamped with today's date. Away from my desk it's harder, so I lean on two simple tools: a single note pinned in Notesnook for anything I can type, and AudioPen when my hands are busy β€” I just talk, and it transcribes. Later, those ideas get folded into the list too.

Either way, next time I open the project the new idea is right there in the briefing next to everything else, dated, so I can see what's fresh and what's been waiting.

The thing that used to kill the list now feeds it.

What makes it work: dates

The whole thing runs on one simple rule β€” every item has dates:

- [ ] Fix pressure bug β€” wrong at altitude (added: 2026-06-20)
- [ ] ⏳ Decide on the logo (added: 2026-06-04)
- [x] Indexable city pages (done: 2026-06-08)

Dates are what let a machine handle it. Claude doesn't have to guess whether something's old β€” it just subtracts two dates. "Stale" stops being a feeling and becomes math.

What changed

The list went from a graveyard to a dashboard. I open the project and the first thing I see is the truth: what moved, what's stuck, what I'm avoiding. I haven't checked off a box by hand in weeks.

The bigger lesson: if you have to remember to do the upkeep, you won't. Tie it to something that already happens, give it a format a machine can read, and let the machine do the boring part.

Takeaways

  • A list dies the moment it stops matching reality.
  • When trust goes, the list goes. Catch it lying once and you stop opening it β€” and a list you don't open is already dead.
  • Don't rely on discipline. Tie upkeep to events you can't skip β€” opening the project, committing code.
  • Make the list show up on its own. A file you have to remember is a file you'll forget.
  • Use dates. They turn "is this stale?" into simple math a machine can do.

When did you last trust your own TODO list? Mine I ignored for years β€” until I stopped being the one keeping it.

Read Full Post β†’

I Gave Claude Code Google Docs-Style Comments

Pixel art illustration of a markdown document with speech-bubble comments pinned to the text

I Gave Claude Code Google Docs-Style Comments

Part of Building GribGrab β€” a series on what I learn building a weather app with Claude Code.

Google Docs has this collaboration system I love: you highlight a line, drop a comment, and bounce back and forth in the margin β€” suggestions, questions, replies. It's the back-and-forth that makes it work.

That's the one thing I missed when I moved most of my work into Claude Code. The drafts were good, but I had no clean way to say "this part β€” fix this."

So I rebuilt the workflow.

My Flow

It's simpler than it sounds:

  1. I use VSCode to structure my folders, work on files, and talk to Claude Code through the built-in terminal.
  2. When I start a project, topic, or task, I create a Markdown file.
  3. The structure is always the same: goal, resources, outcome.
  4. I ask Claude Code to work on that file and draft the outcome right there.
  5. When I want to comment on something, I highlight the text and hit Cmd+Option+M β€” the same shortcut Google Docs uses for inserting a comment.
  6. Then I tell Claude Code to act on the comments.
  7. I iterate on the same file by repeating steps 5 and 6.

The whole thing lives in one Markdown file. Goal at the top, the draft below, my comments inline. No context lost, no copy-pasting between a chat window and an editor.

How to Set It Up

This is the part that makes it click. There's no extension β€” it's a single VSCode keybinding that wraps whatever text I've selected in an HTML comment addressed to Claude.

Open the Command Palette (Cmd+Shift+P), run "Preferences: Open Keyboard Shortcuts (JSON)", and add this:

{
  "key": "alt+cmd+m",
  "command": "editor.action.insertSnippet",
  "when": "editorTextFocus",
  "args": {
    "snippet": "<!-- Claude, this is my comment on the quoted text. Quote: \"${TM_SELECTED_TEXT}\" | My comment: $0 -->"
  }
}

Now highlight any line, press Cmd+Option+M, and you get:

<!-- Claude, this is my comment on the quoted text. Quote: "the highlighted line" | My comment: β–Œ -->

The cursor lands right where you type your note. Two things make this work:

  • ${TM_SELECTED_TEXT} pulls in whatever you've selected, so Claude knows exactly which line you mean.
  • It's an HTML comment (<!-- -->), so it stays invisible when the Markdown renders but Claude reads it fine.

Write your comment, then tell Claude Code to go through the file and address them. It reads the quote, reads your note, and edits in place β€” just like resolving comments in a doc.

Why It Works

The magic isn't the keybinding. It's that comments turn a one-shot prompt into a conversation anchored to the text.

Instead of describing which paragraph I mean in the terminal, I point at it directly. Instead of re-explaining context, the context is the file. The draft and the feedback live in the same place, and that place is version-controlled, searchable, and mine.

It's the Google Docs margin, minus Google Docs.

Takeaways

  • Keep one file per task. Goal, resources, outcome β€” that's the whole structure.
  • Comment inline, don't re-prompt. Anchoring feedback to specific lines beats describing them in the terminal.
  • Steal the shortcut you already know. Mapping it to Cmd+Option+M means there's nothing new to remember.
  • It's one keybinding. No extensions, no setup tax.

How do you give feedback to your AI assistant? Still typing it all in the chat β€” or have you found a better margin?

Read Full Post β†’

I Built a Weather App

Pixel art of a weather chart on a retro screen with rain clouds and a data grid in the background

I Built a Weather App, which I use every day.

This is the start of a series about what I learn while building GribGrab β€” a free hourly weather chart I made with Claude Code. Notes on the product, the process, and the tools along the way.

The series so far

  1. I Built a Weather App β€” the origin story (you're here)
  2. The TODO List Nobody Maintains β€” keeping a project list honest without touching it
  3. I Gave Claude Code Google Docs-Style Comments β€” editing drafts inline, like in a doc
  4. The Analytics Were Mostly Me β€” finding and cutting your own traffic out of your own stats
  5. The Feature That Found a Bug β€” how a new map picker uncovered a bug hiding at the poles

I got tired of weather apps that just say "partly cloudy."

So I built GribGrab β€” a simple page that shows you a full hourly chart for the next 7 days. Temperature, rain, wind, pressure, all in one view. No account, no app. Just search a city and see what's coming hour by hour.

Why

I commute by bike. I travel. I plan outdoor activities. I want to know when exactly it's going to rain on Thursday β€” not just "Thursday: showers."

Most of weather app I tried wrapped that in so much UX that the actual data was buried.

So I built the thing I wanted.

The Reddit Moment

I posted GribGrab on r/weather. Someone replied β€” not with "nice job" β€” but with a detailed explanation of why raw numerical model output shouldn't really be called a forecast.

The short version: global weather models run on grids of roughly 10–25 km per cell. Your city and the hills 5 km away share the same cell. Local effects β€” coastal fog, mountain valleys, urban heat islands β€” are invisible to the model. Real forecasts apply statistical corrections trained on years of local observation data. GribGrab does none of that.

He was right.

What I Did With That

First, I updated the app's language. "Model output" instead of "forecast." A small disclaimer. Honest framing.

Then I realised this is actually a feature for the right audience. Hikers, people outdooring a lot β€” people who understand weather β€” often prefer raw model data. They want to interpret it themselves, not get a smoothed-over summary.

Where It Is Now

About 20–30 people use it daily, mostly from Reddit posts. I've added city pages for SEO β€” early days, but a long-term bet.

No monetization yet. Growth first.

What I Learned

Build the thing that scratches your own itch. Post it publicly as soon as it's embarrassing enough. The criticism you get will teach you a lot.


Have you checked what the weather looked like the last time your hike got rained out? β†’ gribgrab.com

Read Full Post β†’

Are You Sure About That? The Prompt That Changed How I Use AI

Pixel art illustration of a skeptical character questioning a glowing AI screen

Are You Sure About That? The Prompt That Changed How I Use AI

"Are you sure about that?"

This should have been the first thing I said to AI after every initial answer. Why? It gives better responses when I doubt it from the start. I've tried it on several prompts β€” and the difference is real.

Why Skepticism Works

AI models are trained to be helpful. That sometimes means they confidently fill gaps with plausible-sounding information. When you challenge an answer, you force the model to reconsider β€” to flag what it actually knows versus what it's guessing.

The Prompt I Use

I've found two versions useful, depending on how much control I want.

Short version

Always flag uncertain or speculative claims with [uncertain] or phrases like "I'm not sure about this." State well-established facts directly. Don't inflate responses with loosely supported points.

Long version

When answering questions, clearly distinguish between what you know with confidence and what is uncertain. For well-established facts, state them directly. For anything speculative, poorly documented, or based on indirect reasoning, flag it explicitly using phrases like "I'm not certain", "this is speculative", or a marker like [uncertain]. Do not pad responses with loosely related points just to seem thorough. If you don't know something, say so plainly.

Both versions do the same job. The short one fits in a system prompt easily. The long one leaves no ambiguity.

What Changes

Once you add this to your setup, you start seeing [uncertain] markers appear. You notice the model hedging more. You trust what's left unhedged a little more.

It's not that the AI suddenly becomes smarter. It's that it stops pretending to be more confident than it is.


What's your experience? Try it yourself and see what changes.

Read Full Post β†’

I Read a Lot. I Remember Almost Nothing. So I Built This.

Pixel art illustration of a book opening with a single glowing idea floating out

I Read a Lot. I Remember Almost Nothing. So I Built This.

This is a series about building MarrowMinded β€” one AI lesson a day from books I've already read, made with Claude Code. Notes on the product, the process, and the tools along the way.

The series so far

  1. I Read a Lot. I Remember Almost Nothing. So I Built This. β€” the origin story (you're here)
  2. Which AI Writes the Best Lesson? I Made Them Grade Each Other β€” four models grade each other's lessons
  3. The Email My App Never Sent β€” a daily email digest, from a one-inbox MVP to the full n8n flow

I finish a book. I feel good about it. Two weeks later, I can barely recall what it was about.

That's the problem. And it's not a lack of effort β€” I read consistently. The problem is the format. You read a book once, in big chunks, and then... nothing. The ideas don't stick.

I'd read about the Ebbinghaus forgetting curve before β€” the idea that we forget roughly 70% of what we learn within 24 hours. I knew about it. I just didn't do anything about it.

So I built MarrowMinded. The name comes from the idea of extracting marrow β€” the most nutritious part of the bone. Books have marrow too. Most of what surrounds it is just filler.

What It Does

MarrowMinded gives me one lesson per day from a book I've already chosen. Not a summary. Not a chapter. One concept, structured into seven sections:

  • A short quote from the text
  • The key thesis in one sentence
  • The idea explained in depth
  • A specific example from the book
  • How it applies to my life today
  • A concrete task for the day (5–30 minutes)
  • A reflection question

That's it. One lesson. Every day.

How does it look?

MarrowMinded in action

Why One Lesson?

Business books are mostly padding. An author has 40 pages of insight and writes 300 pages of anecdotes around them. The anecdotes make it readable. They also make it forgettable.

The insight from Effortless or Naval's Almanack is there. The signal is real. But the format β€” read it once, all at once β€” is broken.

One focused concept a day, repeated over months, beats a 3-hour reading binge I won't repeat or remember. Drip beats flood.

How I Built It

The stack is deliberately minimal. One Python file (FastAPI + SQLite), one HTML file with vanilla JavaScript. No framework, no build step. Books are plain .txt files. Lessons are generated once by AI and cached as JSON β€” so on day one of any book, there's no API cost at all.

Multi-provider AI support under the hood (Claude, GPT, Gemini, Groq), but Claude is the default. The prebuilt lessons cost me around $50 in tokens total. The whole thing took 10–12 hours to build, mostly evenings.

It runs on Render's free tier.

The Part That Surprised Me

I use it every morning. That's the metric I actually care about.

Building something you genuinely use changes everything about how you build it. You cut the features you don't need because you'd notice if they were wrong. You keep it simple because complexity would slow you down before breakfast.

I have eight books loaded in right now β€” a mix of English and Polish titles. Every morning, one lesson, one task. That's the whole thing.


Do you have a reading habit? And more importantly β€” do you actually remember what you read?

If you're curious how it works, have questions about the stack, or want to build something similar β€” ask. And if you'd like to take a look at the repo, just say so.

Read Full Post β†’

Siri Feels Like 2014β€”And Apple Doesn't Seem to Care

Pixel art illustration of outdated AI assistant vs modern AI

Siri Feels Like 2014β€”And Apple Doesn't Seem to Care

I've been using Opera Neon on my laptop. Together with Claude Code and Claude, I can do a lot. Research, writing, coding, data analysisβ€”you name it. The AI works so well that I forget it's even AI.

Then I ask Siri to do something.

"I can't do that."

The contrast is jarring.

What Siri Can Actually Do

Don't get me wrongβ€”Siri is useful for the basics:

  • "Open app X"
  • "Set a timer for 10 minutes"
  • "Volume up"
  • "Call John"

These work fine. Siri handles simple commands without friction.

But the moment you ask for anything more complex? It falls apart.

Where Siri Breaks Down

Last week, I asked Siri to create a card in Trello.

"I can't do that."

Fine. How about creating a reminder with a specific note attached?

"I can't do that."

These aren't edge cases. These are basic productivity tasks. Things I do daily with other AI tools without even thinking about it.

The Dissonance Is Real

Here's the thing: I've had voice conversations with Perplexity. It feels like watching "Her" v0.1. Not perfect, but you can see where it's going. It responds naturally. It understands context. It adapts.

Having a conversation with Siri feels like talking to a primitive chatbot from 10 years ago. More frustrating than helpful.

And that dissonanceβ€”the gap between what top-tier AI can do and what Siri offersβ€”shows just how far behind Apple and Google really are.

What This Means

I'm not saying Siri needs to match Claude's reasoning capabilities overnight. But the difference isn't just about features. It's about expectations.

When you use Claude, Opera Neon, or even ChatGPT daily, you start expecting AI to understand you. To adapt. To work with context and nuance.

Siri doesn't do that. It's still a list of hardcoded commands wrapped in a voice interface.

Imagine the Alternative

Think about how good Apple's OS could be if their AI were even close to Claude's level.

Not as good. Just close.

Imagine asking your phone a complex question and getting a thoughtful, contextual answer instead of "Here's what I found on the web."

Imagine voice assistants that actually assist instead of just executing preset commands.

We're not there yet. But some of us are getting close.

Just not Apple.


What's your experience? Do you still use Siri regularly? Or have you noticed the same gap I have? I'd love to hear your take.

Read Full Post β†’