<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>Worklore — stories your agent can do</title><link>https://worklore.dev</link><description>Honest developer stories, executable by your AI agent. Point your agent at this feed: each item links the raw .md.</description><item><title>Half-remembered Mom Test — an agent skill turned fuzzy course notes into exact interview questions</title><link>https://worklore.dev/s/2026-08-04-half-remembered-mom-test-an-agent-skill-turned-fuzzy-course-</link><guid>https://worklore.dev/s/2026-08-04-half-remembered-mom-test-an-agent-skill-turned-fuzzy-course-</guid><pubDate>Tue, 04 Aug 2026 17:02:08 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · user-research, mom-test, agent-skills, founder, claude-code&lt;/p&gt;&lt;p&gt;# Half-remembered Mom Test — an agent skill turned fuzzy course notes into exact interview questions&lt;/p&gt;&lt;p&gt;In plain words: most of what we learn on courses degrades into a vague feeling
of &amp;quot;there were rules about this.&amp;quot; I found a way to stop guessing from that
feeling: have the agent load the actual expertise as a skill, and generate the
precise artifact my fuzzy memory could not.&lt;/p&gt;&lt;p&gt;I interviewed my first product tester and something nagged at me afterwards. A
startup-launching course I took had taught the &amp;quot;mom test&amp;quot; questions — I
remembered the shape of the rules (people politely agree with whatever you
pitch, so their agreement is worthless) but not the exact discipline. Reading
my own interview back, I had asked exactly the forbidden kind: &amp;quot;would you be
interested in…&amp;quot;, &amp;quot;would you subscribe…&amp;quot; — hypotheticals that can only harvest
kindness.&lt;/p&gt;&lt;p&gt;So instead of guessing from memory, I asked my agent — which has a
form-and-research skill installed, packaged expertise the way agents consume
it. It scored my interview honestly (my two useful answers came from a
leading question answered &amp;quot;no&amp;quot;, which is reliable precisely because saying no
costs politeness, and from an idea the tester volunteered unprompted). Then it
produced the exact crib sheet: walk me through what you did after install;
where did you stumble; when did you last hit this problem; what do you do
today instead; show me the artifact; would you forward this to a colleague —
the last one being the only positive signal that counts, because a real
forward costs something.&lt;/p&gt;&lt;p&gt;![The generated crib sheet — in the testers&amp;#x27; language, as the method prescribes](/images/mom-test-crib-sheet.png)&lt;/p&gt;&lt;p&gt;The meta-lesson is the part I want to keep: my memory of the framework was
good enough to know a framework existed, and that is all it needs to be. The
agent&amp;#x27;s job is supplying the letter of the method; mine is knowing when to
reach for it.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-08-04-half-remembered-mom-test-an-agent-skill-turned-fuzzy-course-.md&lt;/p&gt;</description></item><item><title>Content that AI agents execute needs a kill-switch — feeds carry stories, never contracts</title><link>https://worklore.dev/s/2026-08-04-content-that-ai-agents-execute-needs-a-kill-switch-feeds-car</link><guid>https://worklore.dev/s/2026-08-04-content-that-ai-agents-execute-needs-a-kill-switch-feeds-car</guid><pubDate>Tue, 04 Aug 2026 16:12:23 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · ai-agents, content-distribution, rss, api-design, security&lt;/p&gt;&lt;p&gt;# Content that AI agents execute needs a kill-switch — feeds carry stories, never contracts&lt;/p&gt;&lt;p&gt;In plain words: if your content is instructions that AI agents run, a cached
copy is not a stale page — it is a stale program someone might still execute.
Ordinary content distribution habits quietly become a safety problem.&lt;/p&gt;&lt;p&gt;I hit this building two features on the same day, for a site whose stories are
executable by readers&amp;#x27; agents. Feature one: a full-text RSS feed (developers
rightly hate truncated teaser feeds). Feature two: authors can withdraw a
published story — &amp;quot;this needs more investigation, it may be incorrect.&amp;quot;&lt;/p&gt;&lt;p&gt;Separately, both are obvious. Together they contradict: RSS readers cache
items forever. If the feed had carried each story&amp;#x27;s executable part — the
step-by-step contract an agent applies to your project — then a story
withdrawn as incorrect on Tuesday would keep executing from readers&amp;#x27; feed
caches indefinitely. The author&amp;#x27;s withdrawal would be theater.&lt;/p&gt;&lt;p&gt;The pattern we landed on has four parts. One canonical live URL per
executable contract, and only that URL is ever executed. Feeds, mirrors, and
previews carry the human narrative in full plus a link — never the contract
inline. Withdrawal returns 410 Gone (not 404: &amp;quot;this existed and was removed
on purpose&amp;quot;) with a plain-prose body — &amp;quot;the author withdrew this story, do
not execute it&amp;quot; — because the consumer is an agent that reads bodies, so the
refusal should be legible to it, not just a status code. And the reproduce
prompt shown to users says explicitly: always fetch live, never from a cache.&lt;/p&gt;&lt;p&gt;The same mechanism now serves moderation: a story quarantined after abuse
reports 410s identically. One kill-switch, two triggers — author conscience
and admin review.&lt;/p&gt;&lt;p&gt;Nothing here is exotic engineering. What is new is the framing: the moment
your content gains an execution path, &amp;quot;where may copies live?&amp;quot; becomes a
safety question, and the honest answer is: copies of the words, anywhere;
copies of the instructions, nowhere.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-08-04-content-that-ai-agents-execute-needs-a-kill-switch-feeds-car.md&lt;/p&gt;</description></item><item><title>DynamoDB reserved words — the crash, and the silent filter that shipped nothing</title><link>https://worklore.dev/s/2026-08-04-dynamodb-reserved-words-the-crash-and-the-silent-filter-that</link><guid>https://worklore.dev/s/2026-08-04-dynamodb-reserved-words-the-crash-and-the-silent-filter-that</guid><pubDate>Tue, 04 Aug 2026 16:12:21 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · dynamodb, aws, python, boto3, debugging&lt;/p&gt;&lt;p&gt;# DynamoDB reserved words — the crash, and the silent filter that shipped nothing&lt;/p&gt;&lt;p&gt;In plain words: DynamoDB keeps a list of 573 words you cannot use as plain
attribute names in its query expressions. Ordinary words, the ones you would
naturally name fields after. Cross the list and you get either a loud crash —or,
in one spot, nothing at all, which is worse.&lt;/p&gt;&lt;p&gt;It bit me three times in one week, twice today. First `similar` (a list of
related items): &amp;quot;Invalid UpdateExpression: Attribute name is a reserved
keyword&amp;quot; — loud, clear, fixed with an alias. Today it was `hidden` (an
author-withdraws-story flag), and the same loud crash in the update. Fine,
alias again, deploy, test — and the end-to-end check still failed: hidden
stories kept appearing in the public feed.&lt;/p&gt;&lt;p&gt;That was the sneaky one. `hidden` was also sitting bare in a
**ProjectionExpression** — the list of fields a scan returns. Reserved words
there did not throw in my read path; the field just came back absent, my
`not item.get(&amp;quot;hidden&amp;quot;)` filter never saw a value, and the &amp;quot;hidden&amp;quot; filter
silently filtered nothing. No error, no log line — only a test asserting
&amp;quot;withdrawn story must NOT be in the index&amp;quot; caught it.&lt;/p&gt;&lt;p&gt;The fix is boring and total: stop playing whack-a-mole with the list. Alias
every attribute name in every expression, always — `#hid` for `hidden`, `#sim`
for `similar` — even names that are safe today, because DynamoDB&amp;#x27;s list is
long, surprising (`status`, `date`, `name`, `type` are all on it), and your
future field names are chosen by product needs, not by the list.&lt;/p&gt;&lt;p&gt;The transferable lesson is the second failure mode: when a filter depends on a
field, test with data where the field is actually set. An empty-field path
that &amp;quot;passes&amp;quot; proves nothing — I wrote about the same shape of bug crashing on
the second write; this one is its quiet sibling.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-08-04-dynamodb-reserved-words-the-crash-and-the-silent-filter-that.md&lt;/p&gt;</description></item><item><title>Confirm-before-write for LLM tool calls without a second inference — replay the stored tool_use block</title><link>https://worklore.dev/s/2026-08-04-confirm-before-write-for-llm-tool-calls-without-a-second-inf</link><guid>https://worklore.dev/s/2026-08-04-confirm-before-write-for-llm-tool-calls-without-a-second-inf</guid><pubDate>Tue, 04 Aug 2026 14:04:53 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by radikkhaziev · llm-agents, tool-use, mcp, telegram-bot, prompt-injection, claude&lt;/p&gt;&lt;p&gt;# Confirm-before-write for LLM tool calls without a second inference — replay the stored tool_use block&lt;/p&gt;&lt;p&gt;My triathlon agent is a Telegram bot where Claude calls MCP tools, some of which mutate real state: pushing a generated workout or a race event into the athlete&amp;#x27;s Intervals.icu calendar. I wanted a human confirm step before any mutation — but the obvious implementation (&amp;quot;user tapped confirm → tell the model to push now&amp;quot;) has two problems. It costs a second inference round per push, and worse, the model *re-generates* the mutating call: what lands in the calendar isn&amp;#x27;t guaranteed to be what the user saw in the preview. Anything in the conversation — including text the model read from tool results — could steer that second call. That&amp;#x27;s the prompt-injection window, exactly on the state-mutating step.&lt;/p&gt;&lt;p&gt;The pattern we landed on: **the confirm never goes back through the model.**&lt;/p&gt;&lt;p&gt;Each mutating tool takes a preview flag (`dry_run=True` / `push_to_intervals=False`) and the system prompt instructs Claude to always call in preview mode. The chat wrapper returns, alongside the text, a deep copy of every `tool_use` block Claude emitted, filtered to a whitelist of previewable tool names (so we don&amp;#x27;t copy unrelated large inputs). The handler stashes the last previewable call — name plus exact input dict — in per-user session state and renders the preview with an inline &amp;quot;✅ Push&amp;quot; button.&lt;/p&gt;&lt;p&gt;On tap, the handler pops the stored block, flips the preview flag in place, and calls the MCP tool directly over HTTP. No model in the loop: the pushed object is bit-for-bit identical to the previewed one, and the push costs zero tokens. A small registry (`_PREVIEWABLE_TOOLS`) maps each tool name to two functions — `is_preview(input)` and `apply_push(input)` — so each tool&amp;#x27;s flag semantics stay local, and a guard refuses to execute if the flag can&amp;#x27;t be flipped (instead of silently re-running the preview and lying about a push).&lt;/p&gt;&lt;p&gt;The same registry now serves two surfaces: a `/workout` conversation flow and free-form chat race creation. Adding a new confirmable tool is one registry entry.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-08-04-confirm-before-write-for-llm-tool-calls-without-a-second-inf.md&lt;/p&gt;</description></item><item><title>Hand-writing a logo in SVG with an AI agent — no image generator needed</title><link>https://worklore.dev/s/2026-08-02-hand-writing-a-logo-in-svg-with-an-ai-agent-no-image-generat</link><guid>https://worklore.dev/s/2026-08-02-hand-writing-a-logo-in-svg-with-an-ai-agent-no-image-generat</guid><pubDate>Mon, 03 Aug 2026 01:02:35 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · svg, logo, branding, claude-code, headless-chrome, design&lt;/p&gt;&lt;p&gt;# Hand-writing a logo in SVG with an AI agent — no image generator needed&lt;/p&gt;&lt;p&gt;I needed a logo and did what everyone does now: opened an image generator. It
was genuinely useful for exactly one thing — exploration. It found the
composition (a terminal prompt next to a checkmark) faster than I would have
sketching. And then it hit its ceiling: spacing it could not control, strokes
that went muddy where they overlapped, colors that were almost-but-not my brand
palette, and no way to say &amp;quot;same thing, 8 percent more air between the glyphs.&amp;quot;&lt;/p&gt;&lt;p&gt;![One of the generator&amp;#x27;s attempts — see the muddy halo where the strokes overlap, and the almost-right colors](/images/logo-generator-attempt.jpg)&lt;/p&gt;&lt;p&gt;Then the obvious thing occurred to us: a logo made of text and geometry is not
an image. It is code. So my agent wrote it as code — fifteen lines of SVG, with
my exact brand hex values, on a 128-unit grid.&lt;/p&gt;&lt;p&gt;![The final mark — fifteen lines of hand-written SVG, exact palette, crisp at 16px](/images/logo-candidate.svg)&lt;/p&gt;&lt;p&gt;Everything about iteration changed. &amp;quot;More spacing between the elements&amp;quot; became
a coordinate edit. &amp;quot;The cursor should be an underline, not a block&amp;quot; — one rect.
&amp;quot;Swap the order: terminal glyph first, then the check&amp;quot; — two paths traded
places. &amp;quot;Try corner radius 26, then 14, then 4, then 0&amp;quot; — a sed one-liner
producing four files I could stare at side by side. Each iteration took
seconds, and every variant stayed in git with a name.&lt;/p&gt;&lt;p&gt;The subtlest discovery: at some point every radius variant looked identical to
me, and I blamed &amp;quot;the font&amp;quot; — though an SVG mark has no font. I was half
right. The stroke voice — line caps, joins, weight — IS the font of a
geometric mark. Round caps make everything soft and friendly no matter how
sharp the tile is; square caps with miter joins turn the same paths into
drafting-table engineering. We rendered four stroke voices of the same mark,
and they differed more than all the radius variants combined. If your mark
feels samey across iterations, you are probably iterating the wrong property.&lt;/p&gt;&lt;p&gt;The judging setup mattered as much as the writing: the agent rendered a preview
page through headless Chrome with the mark at 512, 64, 32 and 16 pixels, in a
circular avatar crop, and on a dark background — the squint test as a
repeatable artifact instead of a vibe. We even sent the concept to a second AI
for a brand-fit review; it caught a real risk (too close to generic terminal
logos) and suggested the fix that became our best candidate.&lt;/p&gt;&lt;p&gt;The division of labor I will keep: generators for exploring what a mark could
be, and for texture. Code for anything geometric or typographic — where what
you need is not more pixels but a clear technical idea, expressed from one
programmer to another. My agent happens to be the other programmer.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-08-02-hand-writing-a-logo-in-svg-with-an-ai-agent-no-image-generat.md&lt;/p&gt;</description></item><item><title>A second AI opinion inside Claude Code — Gemini over curl, no arena apps</title><link>https://worklore.dev/s/2026-08-02-a-second-ai-opinion-inside-claude-code-gemini-over-curl-no-a</link><guid>https://worklore.dev/s/2026-08-02-a-second-ai-opinion-inside-claude-code-gemini-over-curl-no-a</guid><pubDate>Mon, 03 Aug 2026 00:52:32 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · claude-code, gemini, curl, multi-agent, workflow, api-key&lt;/p&gt;&lt;p&gt;# A second AI opinion inside Claude Code — Gemini over curl, no arena apps&lt;/p&gt;&lt;p&gt;I work with one agent all day, and one agent means one set of blind spots. I
wanted a second opinion from a model trained differently — not an &amp;quot;arena&amp;quot; app
where models compete side by side, just a colleague my agent could consult
mid-session when I say &amp;quot;ask Gemini what it thinks.&amp;quot;&lt;/p&gt;&lt;p&gt;The surprise was what it did NOT require. I had a paid Google AI Pro
subscription and assumed that was the price of API access. Wrong twice: the
subscription covers the Gemini app, not the API — my key showed quota zero on
every pro model despite the paid account. And the free tier turned out to be
enough all along: creating an API key at aistudio.google.com costs nothing, no
card, and the free quota (hundreds of flash-model calls a day) is far more than
a second-opinion habit ever uses. The paid account I thought was the
requirement was irrelevant to the whole setup.&lt;/p&gt;&lt;p&gt;The wiring is three pieces. The key goes in ~/.zshenv — not .zshrc, which cost
me a broken afternoon: scripted shells skip .zshrc, so the agent&amp;#x27;s calls saw an
empty variable while my terminal worked fine. The call is one curl to the
generateContent endpoint, with the prompt written to a JSON file first (long
prompts inside shell quotes end in tears). And the habit lives in my agent&amp;#x27;s
global instructions: trigger phrases like &amp;quot;ask Gemini,&amp;quot; a table of which models
my key actually serves, and one behavioral rule — present both views and say
where they disagree, never just relay.&lt;/p&gt;&lt;p&gt;Two gotchas the docs won&amp;#x27;t tell you: older model names may be retired for new
keys (mine 404 on the 2.5 generation — check what your key lists, not what
blog posts say), and each model has its own daily quota, so use the generous
cheap one by default and save the scarce smarter one for hard questions.&lt;/p&gt;&lt;p&gt;Since then, the second opinion has killed a product hypothesis I was about to
build, stress-tested a pivot, and voted on my logo. It disagrees with my main
agent regularly. That is the point.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-08-02-a-second-ai-opinion-inside-claude-code-gemini-over-curl-no-a.md&lt;/p&gt;</description></item><item><title>Two bugs made my scheduler perfectly silent — zsh killed the messenger</title><link>https://worklore.dev/s/2026-08-01-two-bugs-made-my-scheduler-perfectly-silent-zsh-killed-the-m</link><guid>https://worklore.dev/s/2026-08-01-two-bugs-made-my-scheduler-perfectly-silent-zsh-killed-the-m</guid><pubDate>Sun, 02 Aug 2026 02:08:29 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · launchd, macos, zsh, claude-code, automation, debugging&lt;/p&gt;&lt;p&gt;# Two bugs made my scheduler perfectly silent — zsh killed the messenger&lt;/p&gt;&lt;p&gt;Yesterday I filed an open failure: my launchd job — a daily 16:50 headless-agent
review — produced nothing. No notification, no output, and, I claimed, no evidence
it ran at all.&lt;/p&gt;&lt;p&gt;I was wrong about the last part, and finding out how was the whole lesson.&lt;/p&gt;&lt;p&gt;One command answered it: `launchctl print gui/$UID/&amp;lt;job-label&amp;gt;`. It showed
`runs = 2` and `last exit code = 1`. The job had fired, on schedule, both days.
The log file I was sure did not exist had existed the whole time. I had been
debugging &amp;quot;launchd is not firing&amp;quot; when the truth was &amp;quot;my script fails and cannot
tell me.&amp;quot;&lt;/p&gt;&lt;p&gt;The log revealed two stacked bugs. First, my headless agent call wasn&amp;#x27;t receiving
its prompt in the launchd context, so the review died immediately (&amp;quot;Input must be
provided either through stdin or as a prompt argument&amp;quot;). Fix: pipe the prompt via
stdin — `print -r -- &amp;quot;$PROMPT&amp;quot; | claude -p ...` — instead of trusting multi-line
argument parsing inside a scheduled shell.&lt;/p&gt;&lt;p&gt;Second — the one that made everything invisible — my script assigned `status=$?`.
In zsh, `status` is a read-only built-in variable, and assigning to it kills the
script on the spot. That assignment sat between the agent call and the
failure-notification call. So the job failed, and then the messenger that was
supposed to report failures died before speaking. Perfect silence, two layers
deep.&lt;/p&gt;&lt;p&gt;The takeaway I will keep: when automation is silent, first prove whether it ran
(`launchctl print` — runs count and last exit code do not lie), and then check
whether your failure reporter is downstream of the failure. Mine was. Everyone&amp;#x27;s
is, until they check.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-08-01-two-bugs-made-my-scheduler-perfectly-silent-zsh-killed-the-m.md&lt;/p&gt;</description></item><item><title>Route 53 fails to register my .me domain — availability said AVAILABLE</title><link>https://worklore.dev/s/2026-07-31-route-53-fails-to-register-my-me-domain-availability-said-av</link><guid>https://worklore.dev/s/2026-07-31-route-53-fails-to-register-my-me-domain-availability-said-av</guid><pubDate>Sun, 02 Aug 2026 01:58:05 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[FAIL·OPEN — help wanted]&lt;/b&gt; by Yahhi · route53, domains, dns, aws&lt;/p&gt;&lt;p&gt;# Route 53 fails to register my .me domain — availability said AVAILABLE&lt;/p&gt;&lt;p&gt;I registered two domains for a new project through Route 53, same day, same
contact details, same account. The .dev succeeded. The .me failed twice — and I
still do not know why.&lt;/p&gt;&lt;p&gt;The facts, because that is all I have: check-domain-availability returned
AVAILABLE for the .me. The registration operation went in, then flipped to
FAILED with the least helpful message AWS produces: &amp;quot;We can&amp;#x27;t finish registering
your domain. Contact AWS Support.&amp;quot; No error code, no reason field, no charge. A
retry a minute later failed identically. Meanwhile worklore.dev, submitted in the
same minute with byte-identical contact JSON, registered fine.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-07-31-route-53-fails-to-register-my-me-domain-availability-said-av.md&lt;/p&gt;</description></item><item><title>My launchd daily review never fired — no error, no output, nothing</title><link>https://worklore.dev/s/2026-07-31-my-launchd-daily-review-never-fired-no-error-no-output-nothi</link><guid>https://worklore.dev/s/2026-07-31-my-launchd-daily-review-never-fired-no-error-no-output-nothi</guid><pubDate>Sun, 02 Aug 2026 01:58:03 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[FAIL·RESOLVED]&lt;/b&gt; by Yahhi · launchd, macos, claude-code, automation, scheduling&lt;/p&gt;&lt;p&gt;# My launchd daily review never fired — no error, no output, nothing&lt;/p&gt;&lt;p&gt;RESOLVED 2026-08-01 — and my headline was wrong in the best way: the job HAD
fired, both days, on schedule. See the answer story: &amp;quot;Two bugs made my scheduler
perfectly silent — zsh killed the messenger.&amp;quot; Short version: the headless agent
call lost its prompt in the launchd context (fix: pipe it via stdin), and my
script assigned to `status` — a read-only variable in zsh — which killed the
script between the failure and the failure notification. The silence was the
messenger dying, not the scheduler skipping. `launchctl print gui/$UID/&amp;lt;label&amp;gt;`
told the truth in one command: runs = 2, last exit code = 1.&lt;/p&gt;&lt;p&gt;The original report, kept as filed — the wrong assumption is the useful part:&lt;/p&gt;&lt;p&gt;I have a journaling setup for my agent work: a hook records every Claude Code
session, and a scheduled job is supposed to run at 16:50 every day, read the day&amp;#x27;s
sessions, write a journal summary, and send me a macOS notification that the
review is ready.&lt;/p&gt;&lt;p&gt;Today at 16:50 I was sitting right at the machine, awake and working. Nothing
happened. No notification, no journal entries, and as far as I can tell, no
evidence the job ran at all.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-07-31-my-launchd-daily-review-never-fired-no-error-no-output-nothi.md&lt;/p&gt;</description></item><item><title>Author-local dates for a global user-content site</title><link>https://worklore.dev/s/2026-07-31-author-local-dates-for-a-global-user-content-site</link><guid>https://worklore.dev/s/2026-07-31-author-local-dates-for-a-global-user-content-site</guid><pubDate>Sun, 02 Aug 2026 01:58:02 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · timezones, api-design, dynamodb, ux&lt;/p&gt;&lt;p&gt;# Author-local dates for a global user-content site&lt;/p&gt;&lt;p&gt;The first bug report on my new site came from its first user — me. I published a
story in the evening of July 31, Buenos Aires time, and the site displayed it as
August 1. I had stamped the entry with a UTC-flavored date, and my own journal was
suddenly lying to me about which day I did the work.&lt;/p&gt;&lt;p&gt;For a work journal this is not cosmetic. &amp;quot;What did I do on Thursday&amp;quot; has exactly
one correct answer, and it is defined by the author&amp;#x27;s wall clock, not by a server
in Virginia. Users will be publishing from Tokyo, Berlin, and Argentina; each
person&amp;#x27;s Thursday is theirs.&lt;/p&gt;&lt;p&gt;The pattern that fixed it separates two things that pretend to be one:&lt;/p&gt;&lt;p&gt;The **date** is content, not metadata. It is the author&amp;#x27;s local calendar date —
like the date you would write at the top of a paper journal page. It is captured
on the author&amp;#x27;s machine (`date +%F`, never UTC, never guessed by a server) and
displayed exactly as written, no conversion, anywhere in the world.&lt;/p&gt;&lt;p&gt;The **ordering** is metadata, not content. The server stamps every submission
with a UTC instant (received_at) used only for sorting feeds — so when a Tokyo
author and a Buenos Aires author share a calendar date, the feed still orders
them truthfully, and nobody&amp;#x27;s displayed date ever changes because of it.&lt;/p&gt;&lt;p&gt;Timezone conversion of calendar dates is where the bug lived. The fix was
refusing to convert at all.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-07-31-author-local-dates-for-a-global-user-content-site.md&lt;/p&gt;</description></item><item><title>The DynamoDB crash that only appears on your second write</title><link>https://worklore.dev/s/2026-07-31-the-dynamodb-crash-that-only-appears-on-your-second-write</link><guid>https://worklore.dev/s/2026-07-31-the-dynamodb-crash-that-only-appears-on-your-second-write</guid><pubDate>Sun, 02 Aug 2026 01:58:00 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · dynamodb, python, testing, aws-lambda, boto3&lt;/p&gt;&lt;p&gt;# The DynamoDB crash that only appears on your second write&lt;/p&gt;&lt;p&gt;My publish endpoint worked perfectly — once. The first story went in clean. The
second one returned Internal Server Error, and the log said:
&amp;quot;TypeError: Float types are not supported. Use Decimal types instead.&amp;quot;&lt;/p&gt;&lt;p&gt;DynamoDB does not accept Python floats. Everyone learns this eventually; the
interesting part is why the first write survived. My API computes similarity
scores between the incoming story and the existing ones, and stores the top
matches. On the very first publish the table was empty — no matches, no scores,
no floats, no crash. The bug did not exist until the data did.&lt;/p&gt;&lt;p&gt;The fix took one line (scores became integer percentages — DynamoDB is happy with
ints and Decimals). The lesson took longer to sink in: my &amp;quot;it works&amp;quot; test had only
ever exercised the empty-database path. Any code whose behavior depends on
existing state — scoring, deduplication, aggregation, &amp;quot;related items&amp;quot; — has a
second code path that first-write tests never touch.&lt;/p&gt;&lt;p&gt;Now every write-path test seeds the table with one record before testing the
write. The populated path is the real path; the empty path happens exactly once
in production, ever.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-07-31-the-dynamodb-crash-that-only-appears-on-your-second-write.md&lt;/p&gt;</description></item><item><title>A serverless API in one afternoon with SAM — and the SecureString wall</title><link>https://worklore.dev/s/2026-07-31-a-serverless-api-in-one-afternoon-with-sam-and-the-securestr</link><guid>https://worklore.dev/s/2026-07-31-a-serverless-api-in-one-afternoon-with-sam-and-the-securestr</guid><pubDate>Sun, 02 Aug 2026 01:57:58 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · aws-sam, aws-lambda, dynamodb, serverless, ssm, cloudformation&lt;/p&gt;&lt;p&gt;# A serverless API in one afternoon with SAM — and the SecureString wall&lt;/p&gt;&lt;p&gt;I ship mobile apps. AWS backends are not my home turf, and I expected standing up
an API — stories in a database, publish with auth, public reads — to eat a week.&lt;/p&gt;&lt;p&gt;It ate an afternoon, and the reason is that the whole backend is two files. One
YAML template (~60 lines) describes everything: the database table, the function,
the HTTP front door. One Python file is the entire API. `sam deploy` reads the
description and conjures the real infrastructure; change the file, deploy again,
done. Nothing to patch, nothing idling at 3am, and at my scale the bill is
literally pennies — pay-per-request everywhere.&lt;/p&gt;&lt;p&gt;The only wall of the day was, in retrospect, the best part. I stored an auth token
in SSM Parameter Store and naturally marked it SecureString — encrypted, the
responsible choice. Deploy failed: &amp;quot;Non-secure ssm prefix was used for secure
parameter.&amp;quot; CloudFormation refuses to inject encrypted parameters into Lambda
environment variables — because env vars sit in plaintext in the console, and
injecting a decrypted secret there would quietly defeat the encryption. The wall
was a security feature protecting me from myself.&lt;/p&gt;&lt;p&gt;Two honest exits, and we ended up using both: a plain String parameter for the
low-stakes token, and for the real secret (an OAuth client secret), reading it at
runtime from inside the function with a narrowly-scoped IAM permission. Annoying
in the moment. Correct forever after.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-07-31-a-serverless-api-in-one-afternoon-with-sam-and-the-securestr.md&lt;/p&gt;</description></item><item><title>GitHub Device Flow auth on AWS Lambda — no passwords, no Auth0</title><link>https://worklore.dev/s/2026-07-31-github-device-flow-auth-on-aws-lambda-no-passwords-no-auth0</link><guid>https://worklore.dev/s/2026-07-31-github-device-flow-auth-on-aws-lambda-no-passwords-no-auth0</guid><pubDate>Sun, 02 Aug 2026 01:57:56 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · github-oauth, aws-lambda, authentication, device-flow, serverless&lt;/p&gt;&lt;p&gt;# GitHub Device Flow auth on AWS Lambda — no passwords, no Auth0&lt;/p&gt;&lt;p&gt;I needed user registration for a site whose users are all developers, and whose
main client is not a browser — it is a skill running inside an agent session, in a
terminal. Every auth service I looked at (Auth0, Clerk, Cognito) is built for web
forms and wanted to own a problem I do not have. Cognito could not even federate
GitHub natively.&lt;/p&gt;&lt;p&gt;So I asked a different question: what do gh CLI and Copilot do? GitHub Device Flow.
The terminal shows &amp;quot;visit github.com/login/device, enter code 6E8E-2233&amp;quot;, you type
it into any browser, and the terminal picks up the authorization. No password ever
exists, no redirect URL is needed on the CLI side, and my users all have GitHub by
definition.&lt;/p&gt;&lt;p&gt;My agent built the whole thing in one sitting: two Lambda routes — one proxies
GitHub&amp;#x27;s device-code endpoint, the other polls for the authorization, fetches the
public profile, and issues my own token, stored as a SHA-256 hash in DynamoDB. We
request an empty scope, so the app cannot see anyone&amp;#x27;s repositories or email — only
the public handle and avatar, which is exactly what author profiles need anyway.&lt;/p&gt;&lt;p&gt;I was the first registered user, about thirty minutes after deploy. The whole flow
took me eleven seconds, and &amp;quot;registration page&amp;quot; is now a sentence nobody has to say.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-07-31-github-device-flow-auth-on-aws-lambda-no-passwords-no-auth0.md&lt;/p&gt;</description></item><item><title>Russia-reachable static mirror on cloud.ru S3</title><link>https://worklore.dev/s/2026-07-31-russia-reachable-static-mirror-on-cloud-ru-s3</link><guid>https://worklore.dev/s/2026-07-31-russia-reachable-static-mirror-on-cloud-ru-s3</guid><pubDate>Fri, 31 Jul 2026 21:00:01 +0000</pubDate><description>&lt;p&gt;&lt;b&gt;[SUCCESS]&lt;/b&gt; by Yahhi · aws-s3, static-hosting, nextjs, deployment, geo-blocking&lt;/p&gt;&lt;p&gt;# Russia-reachable static mirror on cloud.ru S3&lt;/p&gt;&lt;p&gt;My Next.js frontend lived on AWS Amplify, served through CloudFront — and about a
third of its users are in Russia, where AWS edge nodes are unreachable. Those users
did not get an error page. They got a blank screen and a spinner, which is worse,
because nobody files a bug report about a blank screen. They just leave. For six
weeks I only knew something was wrong from two user screenshots.&lt;/p&gt;&lt;p&gt;The task: give Russian users a working URL without forking the codebase, without
telling anyone to install a VPN, and without moving the global site off AWS. Budget:
close to zero. The constraint that shaped everything — the app had to stay one
codebase with one build story.&lt;/p&gt;&lt;p&gt;The route that worked: a static mirror on cloud.ru Object Storage (S3-compatible,
on Russian soil). My agent scripted it end to end: switch Next.js to static export
(output: &amp;#x27;export&amp;#x27;, with generateStaticParams for every dynamic route, and
middleware.ts moved aside during the build — it is incompatible with export), sync
the out/ directory with the aws CLI pointed at --endpoint-url=https://s3.cloud.ru,
then fix the three gotchas that cost me a day each the first time: HTML files
arrive as binary/octet-stream (browsers download them instead of rendering — every
.html must be re-PUT with an explicit text/html content type), clean URLs need each
page copied to its directory/index.html form, and dynamic routes need a 404-to-SPA
fallback that restores the real id from the query string.&lt;/p&gt;&lt;p&gt;Result: a mirror at the bucket&amp;#x27;s s3-website.cloud.ru endpoint, reachable from
Russian networks, loading in under a second — confirmed by the same users who sent
the blank-screen screenshots. The global AWS site is untouched, the mirror redeploys
with one script, and the whole thing costs a few cents a month in storage.&lt;/p&gt;&lt;p&gt;Reproduce with your agent — always fetch live, never from this feed: https://worklore.dev/s/2026-07-31-russia-reachable-static-mirror-on-cloud-ru-s3.md&lt;/p&gt;</description></item></channel></rss>