<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: datasette</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/datasette.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-10-12T04:57:07+00:00</updated><author><name>Simon Willison</name></author><entry><title>datasette-ports 0.4</title><link href="https://simonwillison.net/2026/Oct/12/datasette-ports/" rel="alternate"/><published>2026-10-12T04:57:07+00:00</published><updated>2026-10-12T04:57:07+00:00</updated><id>https://simonwillison.net/2026/Oct/12/datasette-ports/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/datasette/datasette-ports/releases/tag/0.4"&gt;datasette-ports 0.4&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;My plugin for detecting and listing all Datasette instances on the current machine now works with the latest version, and displays the detected PID as well. Knowing the PID makes it easier to &lt;a href="https://simonwillison.net/2026/Oct/12/python-3-15/#a-sampling-profiler"&gt;attach the new Python 3.15 profiler&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/></entry><entry><title>Using Parseable with Datasette for OpenTelemetry traces</title><link href="https://simonwillison.net/2026/Oct/6/datasette-parseable-opentelemetry/" rel="alternate"/><published>2026-10-06T19:07:31+00:00</published><updated>2026-10-06T19:07:31+00:00</updated><id>https://simonwillison.net/2026/Oct/6/datasette-parseable-opentelemetry/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;TIL:&lt;/strong&gt; &lt;a href="https://til.simonwillison.net/datasette/datasette-parseable-opentelemetry"&gt;Using Parseable with Datasette for OpenTelemetry traces&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;I &lt;a href="https://news.ycombinator.com/item?id=49978171"&gt;saw Parseable in a Show HN&lt;/a&gt; today - it's a new observability platform with both an &lt;a href="https://github.com/parseablehq/parseable"&gt;open source (AGPL)&lt;/a&gt; Rust implementation (a single ~180MB binary), an "Enterprise" version with extra features and a cloud hosted option.&lt;/p&gt;
&lt;p&gt;Since &lt;a href="https://docs.datasette.io/en/latest/changelog.html#a41-2026-09-24"&gt;Datasette 1.0a41 added OpenTelemetry support&lt;/a&gt; (thanks, Alex Garcia), I decided to fire up Codex and have it figure out how to run Parseable and feed it traces from Datasette.&lt;/p&gt;
&lt;p&gt;Here's my (human-written) TIL showing the patterns that worked, and here's a screenshot of a Datasette trace displayed within the Parseable localhost web application:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a trace detail view in an observability web app, with a span waterfall overlaid on a dimmed navigation sidebar and filter column. Dimmed sidebar: breadcrumb &amp;quot;Community &amp;gt; Traces &amp;gt; datas&amp;quot; (cut off), search box &amp;quot;Search... ⌘K&amp;quot;, nav items &amp;quot;Home&amp;quot;, &amp;quot;Ingest telemetry&amp;quot;, section &amp;quot;ANALYZE&amp;quot;: &amp;quot;Keystone&amp;quot;, &amp;quot;Dashboards&amp;quot;, &amp;quot;SQL Editor&amp;quot;, section &amp;quot;OBSERVE&amp;quot;: &amp;quot;Logs&amp;quot;, &amp;quot;Metrics&amp;quot;, &amp;quot;Traces&amp;quot; (selected), &amp;quot;APM&amp;quot;, &amp;quot;Agents&amp;quot;, section &amp;quot;MONITOR&amp;quot;: &amp;quot;Alerts&amp;quot;, &amp;quot;Errors&amp;quot;, section &amp;quot;DATA&amp;quot;: &amp;quot;Datasets&amp;quot;, and at the bottom &amp;quot;Settings&amp;quot;, &amp;quot;Book a call&amp;quot;, &amp;quot;Support&amp;quot;. Dimmed filter column, cut off at the right edge: &amp;quot;Search fi&amp;quot;, &amp;quot;Core&amp;quot;, &amp;quot;Log format&amp;quot;, &amp;quot;User agent&amp;quot;, &amp;quot;Source IPs&amp;quot;, &amp;quot;Error&amp;quot;, &amp;quot;Service&amp;quot;, &amp;quot;service.ins&amp;quot;, &amp;quot;service.na&amp;quot;, &amp;quot;datasett&amp;quot; (checked), &amp;quot;Span&amp;quot;, &amp;quot;Database&amp;quot;, &amp;quot;HTTP&amp;quot;, &amp;quot;http.reque&amp;quot;, &amp;quot;NULL&amp;quot; (unchecked), &amp;quot;GET&amp;quot; (checked), &amp;quot;http.respo&amp;quot;, &amp;quot;http.route&amp;quot;, &amp;quot;Server&amp;quot;, &amp;quot;server.add&amp;quot;, &amp;quot;Telemetry&amp;quot;, &amp;quot;URL&amp;quot;, &amp;quot;All fields&amp;quot;. Trace panel header: &amp;quot;Trace detail &amp;gt; 6f819a2170bcd1e91c6ea3ae236ec60b&amp;quot; with a copy icon, a &amp;quot;Related logs&amp;quot; button and a close X. Summary: &amp;quot;Start time 6:56 PM, Oct 6, 2026 UTC&amp;quot;, &amp;quot;Duration 40.9 ms&amp;quot;, &amp;quot;Spans 247&amp;quot;. A minimap with axis &amp;quot;0ns 10.2ms 20.5ms 30.7ms 40.9ms&amp;quot; shows many short span bars cascading diagonally from top left toward the lower middle, with a few longer bars. Below is a span table with a &amp;quot;Span name&amp;quot; header, a &amp;quot;Search spans...&amp;quot; box, collapse and expand buttons, and a timeline axis &amp;quot;0ns 10.2ms 20.5ms 30.7ms 40.9ms&amp;quot;. Rows (name, service, duration): root span with collapse toggle &amp;quot;123&amp;quot;, &amp;quot;GET /...&amp;quot; &amp;quot;datasette...&amp;quot; 40.9ms spanning the full timeline; then alternating rows where each &amp;quot;db.query&amp;quot; has a collapse toggle &amp;quot;1&amp;quot;: db.query datasette-local 679µs, db.query.execute datasette-local 278µs, db.query datasette-local 341µs, db.query.execute datasette-local 55µs, db.query datasette-local 357µs, db.query.execute datasette-local 197µs, db.query datasette-local 330µs, db.query.execute datasette-local 73µs, db.query datasette-local 2.26ms, db.query.execute datasette-local 2.02ms, db.query datasette-local 232µs, db.query.execute datasette-local 64µs, db.query datasette-local 207µs, db.query.execute datasette-local 71µs, db.query datasette-local 6.11ms. The child span bars start progressively later across the early part of the timeline." src="https://raw.githubusercontent.com/simonw/til/refs/heads/main/datasette/datasette-parsable.webp" /&gt;&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observability"&gt;observability&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/opentelemetry"&gt;opentelemetry&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="observability"/><category term="alex-garcia"/><category term="opentelemetry"/></entry><entry><title>datasette-atom 0.11a0</title><link href="https://simonwillison.net/2026/Oct/6/datasette-atom/" rel="alternate"/><published>2026-10-06T17:35:39+00:00</published><updated>2026-10-06T17:35:39+00:00</updated><id>https://simonwillison.net/2026/Oct/6/datasette-atom/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-atom/releases/tag/0.11a0"&gt;datasette-atom 0.11a0&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;A minor fix for compatibility with the latest Datasette alphas. This meant we could upgrade the &lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt; site to Datasette 1.0a41.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/atom"&gt;atom&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="atom"/><category term="datasette"/></entry><entry><title>datasette 1.0a41</title><link href="https://simonwillison.net/2026/Sep/24/datasette/" rel="alternate"/><published>2026-09-24T19:15:23+00:00</published><updated>2026-09-24T19:15:23+00:00</updated><id>https://simonwillison.net/2026/Sep/24/datasette/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a41"&gt;datasette 1.0a41&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;Alec Garcia added &lt;a href="https://docs.datasette.io/en/latest/internals.html#internals-telemetry"&gt;support for OpenTelemetry&lt;/a&gt; to Datasette in this release.&lt;/p&gt;
&lt;p&gt;I've also refactored all of Datasette's modal dialogs to a single Web Component, which is now &lt;a href="https://docs.datasette.io/en/latest/javascript_plugins.html#javascript-plugins-modals"&gt;documented for other plugins to use&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-components"&gt;web-components&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/opentelemetry"&gt;opentelemetry&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="javascript"/><category term="datasette"/><category term="web-components"/><category term="alex-garcia"/><category term="opentelemetry"/></entry><entry><title>datasette-explain 0.2.2</title><link href="https://simonwillison.net/2026/Sep/20/datasette-explain/" rel="alternate"/><published>2026-09-20T00:22:04+00:00</published><updated>2026-09-20T00:22:04+00:00</updated><id>https://simonwillison.net/2026/Sep/20/datasette-explain/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-explain/releases/tag/0.2.2"&gt;datasette-explain 0.2.2&lt;/a&gt;&lt;/p&gt;
        &lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Explain plans now work on read-only stored-query pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I upgraded &lt;a href="https://datasette.simonwillison.net"&gt;datasette.simonwillison.net&lt;/a&gt; to Datasette 1.0a40, which inspired me to ship a new version of this explain plugin.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="sqlite"/><category term="datasette"/></entry><entry><title>datasette-auth-github 1.0</title><link href="https://simonwillison.net/2026/Sep/19/datasette-auth-github/" rel="alternate"/><published>2026-09-19T19:52:02+00:00</published><updated>2026-09-19T19:52:02+00:00</updated><id>https://simonwillison.net/2026/Sep/19/datasette-auth-github/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-auth-github/releases/tag/1.0"&gt;datasette-auth-github 1.0&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;I run this GitHub login plugin on the &lt;a href="https://agent.datasette.io/"&gt;agent.datasette.io&lt;/a&gt; demo site and I noticed that my authenticated sessions weren't lasting very long. It turned out that the plugin was setting cookies without a &lt;code&gt;Max-Age&lt;/code&gt; parameter, so they were expiring at the end of a browser session (which in Mobile Safari seems to happen pretty often, independently of how you are using the app.)&lt;/p&gt;
&lt;p&gt;I fixed that in &lt;a href="https://github.com/simonw/datasette-auth-github/issues/80"&gt;#80&lt;/a&gt; and, since this plugin has been around for quite a while and is tested against both Datasette 0.65.x and Datasette 1.0ax, I decided to bump it up to a 1.0 release. I'm trying to get better at promoting stable plugins to 1.0.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github"/><category term="plugins"/><category term="datasette"/></entry><entry><title>datasette 1.0a40</title><link href="https://simonwillison.net/2026/Sep/16/datasette/" rel="alternate"/><published>2026-09-16T23:51:43+00:00</published><updated>2026-09-16T23:51:43+00:00</updated><id>https://simonwillison.net/2026/Sep/16/datasette/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a40"&gt;datasette 1.0a40&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;Same security fix as &lt;a href="https://simonwillison.net/2026/Sep/16/datasette-2/"&gt;0.65.5&lt;/a&gt;, plus some neat new features and bug fixes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Plugins can now launch and manage &lt;strong&gt;background tasks&lt;/strong&gt; using the new &lt;a href="https://docs.datasette.io/en/latest/internals.html#datasette-add-background-task"&gt;datasette.add_background_task()&lt;/a&gt; method. Thanks, &lt;a href="https://alexgarcia.xyz/"&gt;Alex Garcia&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I've migrated Datasette to &lt;a href="https://github.com/pydantic/httpx2"&gt;httpx2&lt;/a&gt; for features like the internal &lt;code&gt;datasette.client.get()&lt;/code&gt; method.&lt;/li&gt;
&lt;li&gt;A whole lot of &lt;a href="https://docs.datasette.io/en/latest/changelog.html#a40-2026-09-16"&gt;bug fixes&lt;/a&gt;, many of them stemming from a recent effort to triage issues for a 1.0 stable release.&lt;/li&gt;
&lt;/ul&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="security"/><category term="datasette"/></entry><entry><title>datasette 0.65.5</title><link href="https://simonwillison.net/2026/Sep/16/datasette-2/" rel="alternate"/><published>2026-09-16T23:51:08+00:00</published><updated>2026-09-16T23:51:08+00:00</updated><id>https://simonwillison.net/2026/Sep/16/datasette-2/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/0.65.5"&gt;datasette 0.65.5&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;Security fix for an issue where a trailing newline in a requested table name could bypass table permissions and expose private rows, reported by &lt;a href="https://github.com/dpfkdlemtp"&gt;dpfkdlemtp&lt;/a&gt; in &lt;a href="https://github.com/simonw/datasette/security/advisories/GHSA-h547-rmjf-5m2m"&gt;GHSA-h547-rmjf-5m2m&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="security"/><category term="datasette"/></entry><entry><title>Datasette 1.0a39 and 0.65.4 security releases</title><link href="https://simonwillison.net/2026/Sep/11/datasette-security/" rel="alternate"/><published>2026-09-11T03:27:16+00:00</published><updated>2026-09-11T03:27:16+00:00</updated><id>https://simonwillison.net/2026/Sep/11/datasette-security/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://datasette.io/blog/2026/september-security-releases/"&gt;Datasette 1.0a39 and 0.65.4 security releases&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Today we're releasing two new security patch versions of Datasette: &lt;a href="https://docs.datasette.io/en/latest/changelog.html#v1-0-a39"&gt;1.0a39&lt;/a&gt; and &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-65-4"&gt;0.65.4&lt;/a&gt; - one for the current alpha series and one for the stable 0.65.x family.&lt;/p&gt;
&lt;p&gt;These are security fixes which you should apply if you are running a Datasette instance on the public web - in particular if that instance mixes both public and private tables.&lt;/p&gt;
&lt;p&gt;Following issues reported by &lt;a href="https://github.com/jankesec"&gt;Sevban Dönmez&lt;/a&gt;, &lt;a href="https://alexgarcia.xyz"&gt;Alex Garcia&lt;/a&gt; and I ran an extensive audit of Datasette using Claude Fable 5.1, GPT-5.6, and GPT-6 Astra. We then spent almost a week collaborating on and reviewing the fixes.&lt;/p&gt;
&lt;p&gt;They helped find some &lt;em&gt;very&lt;/em&gt; subtle bugs. We'll be incorporating security audits by frontier models into all of our development work going forward.&lt;/p&gt;
&lt;p&gt;Alex came up with a way of splitting the work which I found extremely productive:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Alex Garcia and I worked together running and then responding to the audit, working in a shared private repository. For most of the issues we split the work: one of us would create the automated tests highlighting the issue, then the other would implement the fix. This ensured that two separate humans had eyes on each of the issues, in addition to our coding agents running different models.&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/releases"&gt;releases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/agentic-engineering"&gt;agentic-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-security-research"&gt;ai-security-research&lt;/a&gt;&lt;/p&gt;



</summary><category term="releases"/><category term="security"/><category term="ai"/><category term="datasette"/><category term="generative-ai"/><category term="llms"/><category term="agentic-engineering"/><category term="ai-security-research"/></entry><entry><title>datasette-publish-fly 1.4</title><link href="https://simonwillison.net/2026/Sep/11/datasette-publish-fly/" rel="alternate"/><published>2026-09-11T02:58:52+00:00</published><updated>2026-09-11T02:58:52+00:00</updated><id>https://simonwillison.net/2026/Sep/11/datasette-publish-fly/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-publish-fly/releases/tag/1.4"&gt;datasette-publish-fly 1.4&lt;/a&gt;&lt;/p&gt;
        &lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Sets &lt;code&gt;force_https=true&lt;/code&gt; in &lt;code&gt;fly.toml&lt;/code&gt;. &lt;a href="https://github.com/simonw/datasette-publish-fly/issues/31"&gt;#31&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Fix for Volume could not be found bug. &lt;a href="https://github.com/simonw/datasette-publish-fly/issues/32"&gt;#32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Compatible with app-scoped deploy tokens. &lt;a href="https://github.com/simonw/datasette-publish-fly/issues/34"&gt;#34&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="fly"/></entry><entry><title>datasette 0.65.4</title><link href="https://simonwillison.net/2026/Sep/11/datasette/" rel="alternate"/><published>2026-09-11T00:06:55+00:00</published><updated>2026-09-11T00:06:55+00:00</updated><id>https://simonwillison.net/2026/Sep/11/datasette/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/0.65.4"&gt;datasette 0.65.4&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;See &lt;a href="https://datasette.io/blog/2026/september-security-releases/"&gt;Datasette 1.0a39 and 0.65.4 security releases&lt;/a&gt; on the Datasette blog.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="security"/><category term="datasette"/></entry><entry><title>datasette 1.0a39</title><link href="https://simonwillison.net/2026/Sep/11/datasette-2/" rel="alternate"/><published>2026-09-11T00:05:54+00:00</published><updated>2026-09-11T00:05:54+00:00</updated><id>https://simonwillison.net/2026/Sep/11/datasette-2/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a39"&gt;datasette 1.0a39&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;See &lt;a href="https://datasette.io/blog/2026/september-security-releases/"&gt;Datasette 1.0a39 and 0.65.4 security releases&lt;/a&gt; on the Datasette blog.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="security"/><category term="datasette"/></entry><entry><title>Creepy crawlies</title><link href="https://simonwillison.net/2026/Sep/7/creepy-crawlies/" rel="alternate"/><published>2026-09-07T23:08:58+00:00</published><updated>2026-09-07T23:08:58+00:00</updated><id>https://simonwillison.net/2026/Sep/7/creepy-crawlies/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://people.kernel.org/monsieuricon/creepy-crawlies"&gt;Creepy crawlies&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Konstantin Ryabitsev discusses how bad the "background radiation" of abusive crawlers has become from the perspective of &lt;a href="https://git.kernel.org/"&gt;git.kernel.org&lt;/a&gt;, the official Git repository for the Linux kernel:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TL;DR: we spend more CPU cycles rendering commits for scrapers than we spend on all other kinds of legitimate access, including git clones. At any one time, across 5 geo-distributed nodes, there are 14 CPU cores doing nothing but rendering git commits as html.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I worry about this a lot from the perspective of Datasette, which serves a huge number of crawlable web pages.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=49491791"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/crawling"&gt;crawling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/git"&gt;git&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/linux"&gt;linux&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-ethics"&gt;ai-ethics&lt;/a&gt;&lt;/p&gt;



</summary><category term="crawling"/><category term="git"/><category term="linux"/><category term="datasette"/><category term="ai-ethics"/></entry><entry><title>datasette-mcp 0.2</title><link href="https://simonwillison.net/2026/Sep/1/datasette-mcp/" rel="alternate"/><published>2026-09-01T15:30:12+00:00</published><updated>2026-09-01T15:30:12+00:00</updated><id>https://simonwillison.net/2026/Sep/1/datasette-mcp/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/datasette/datasette-mcp/releases/tag/0.2"&gt;datasette-mcp 0.2&lt;/a&gt;&lt;/p&gt;
        &lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"rows"&lt;/code&gt; from &lt;code&gt;execute_sql&lt;/code&gt; is now an array of objects. Previously it was an array of arrays. This should help weaker models avoid losing track of which positional array element maps to which column. &lt;a href="https://github.com/datasette/datasette-mcp/issues/1"&gt;#1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Now depends on &lt;code&gt;mcp&amp;gt;=2.1.1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the first non-alpha release of the plugin. I'm confident it's ready as I've been using it quite a bit myself.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/model-context-protocol"&gt;model-context-protocol&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="model-context-protocol"/></entry><entry><title>datasette-upload-dbs 0.5a0</title><link href="https://simonwillison.net/2026/Aug/11/datasette-upload-dbs/" rel="alternate"/><published>2026-08-11T20:35:13+00:00</published><updated>2026-08-11T20:35:13+00:00</updated><id>https://simonwillison.net/2026/Aug/11/datasette-upload-dbs/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-upload-dbs/releases/tag/0.5a0"&gt;datasette-upload-dbs 0.5a0&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;This plugin has been around for a while - it lets users upload a brand new SQLite database to a hosted Datasette instance, at which point that database will start being served by that instance.&lt;/p&gt;
&lt;p&gt;It can also be used to atomically swap a database with a more recent version. The uploaded database is saved to a file, verified, then swapped in so &lt;code&gt;/name&lt;/code&gt; starts serving the new one.&lt;/p&gt;
&lt;p&gt;The new release adds a formalized API, so you can replace an existing database (or add a new one) like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Accept: application/json" \
  -F "db=@content.db" \
  -F "db_name=content" \
  https://your-instance.example.com/-/upload-dbs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means you can build fresh databases in an environment such as GitHub Actions and swap them in production as soon as that build has completed.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/></entry><entry><title>datasette-auth-tokens 0.4a13</title><link href="https://simonwillison.net/2026/Aug/6/datasette-auth-tokens/" rel="alternate"/><published>2026-08-06T23:07:47+00:00</published><updated>2026-08-06T23:07:47+00:00</updated><id>https://simonwillison.net/2026/Aug/6/datasette-auth-tokens/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-auth-tokens/releases/tag/0.4a13"&gt;datasette-auth-tokens 0.4a13&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;Upgraded for compatibility with `sqlite-utils 4.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/></entry><entry><title>datasette 1.0a38</title><link href="https://simonwillison.net/2026/Aug/6/datasette/" rel="alternate"/><published>2026-08-06T18:24:34+00:00</published><updated>2026-08-06T18:24:34+00:00</updated><id>https://simonwillison.net/2026/Aug/6/datasette/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a38"&gt;datasette 1.0a38&lt;/a&gt;&lt;/p&gt;
        &lt;blockquote&gt;
&lt;p&gt;This release fixes a &lt;strong&gt;SQL injection&lt;/strong&gt; security issue that affects Datasette instances that serve a &lt;strong&gt;mixture of public and private tables&lt;/strong&gt; in the same database, with access configured using the &lt;a href="https://docs.datasette.io/en/latest/authentication.html"&gt;Datasette permissions system&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Site administrators who serve private tables in this way are advised to disable the &lt;a href="https://docs.datasette.io/en/latest/authentication.html#execute-sql"&gt;execute-sql permission&lt;/a&gt; on that database to prevent users from accessing private tables using raw SQL queries. The bug that has been fixed would have allowed users with access to any public table to execute SQL injection attacks despite that restriction, giving them read-only access to data in private tables in the same database.&lt;/p&gt;
&lt;p&gt;This fix is also available in &lt;a href="https://simonwillison.net/2026/Aug/6/datasette-2/"&gt;Datasette 0.65.3&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Thankfully this particular configuration - private tables and public tables exposed for the same database within the same instance - is likely to be rare. I've not encountered an instance like that myself.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql-injection"&gt;sql-injection&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="security"/><category term="sql-injection"/><category term="datasette"/></entry><entry><title>datasette 0.65.3</title><link href="https://simonwillison.net/2026/Aug/6/datasette-2/" rel="alternate"/><published>2026-08-06T18:22:07+00:00</published><updated>2026-08-06T18:22:07+00:00</updated><id>https://simonwillison.net/2026/Aug/6/datasette-2/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/0.65.3"&gt;datasette 0.65.3&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;Back-ported the SQL Injection security fix from &lt;a href="https://simonwillison.net/2026/Aug/6/datasette/"&gt;1.0a38&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/></entry><entry><title>datasette-apps 0.2a0</title><link href="https://simonwillison.net/2026/Aug/1/datasette-apps/" rel="alternate"/><published>2026-08-01T21:23:56+00:00</published><updated>2026-08-01T21:23:56+00:00</updated><id>https://simonwillison.net/2026/Aug/1/datasette-apps/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/datasette/datasette-apps/releases/tag/0.2a0"&gt;datasette-apps 0.2a0&lt;/a&gt;&lt;/p&gt;
        &lt;blockquote&gt;
&lt;p&gt;Changes that improve Datasette Apps when created and edited using &lt;a href="https://agent.datasette.io/"&gt;Datasette Agent&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;code&gt;app_debug()&lt;/code&gt; tool allowing agent to open an app (invisibly) and test it using JavaScript. &lt;a href="https://github.com/datasette/datasette-apps/pull/33"&gt;#33&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;app_list()&lt;/code&gt; tool for listing apps the user has permission to edit, so the agent can edit them. &lt;a href="https://github.com/datasette/datasette-apps/issues/36"&gt;#36&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;app_debug()&lt;/code&gt; tool is pretty neat: it works by displaying the app in a &lt;code&gt;opacity: 0&lt;/code&gt; iframe with &lt;code&gt;pointer-events: none&lt;/code&gt; (so it can't be seen or interacted with) and then executing agent-provided JavaScript inside that sandboxed iframe. This means the agent can smoke test that the app is working and even do things like measure the dimensions of different elements.&lt;/p&gt;
&lt;p&gt;This uses the new &lt;code&gt;context.browser_task()&lt;/code&gt; mechanism added in &lt;a href="https://simonwillison.net/2026/Jul/31/datasette-agent/"&gt;datasette-agent 0.4a0&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/iframes"&gt;iframes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-apps"&gt;datasette-apps&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="iframes"/><category term="datasette"/><category term="datasette-apps"/></entry><entry><title>Stateless MCP has recaptured my interest (and inspired mcp-explorer and datasette-mcp)</title><link href="https://simonwillison.net/2026/Jul/31/stateless-mcp/" rel="alternate"/><published>2026-07-31T23:13:22+00:00</published><updated>2026-07-31T23:13:22+00:00</updated><id>https://simonwillison.net/2026/Jul/31/stateless-mcp/</id><summary type="html">
    &lt;p&gt;Tuesday was &lt;a href="https://x.com/ade_oshineye/status/2082129440943866149"&gt;Stateless MCP day&lt;/a&gt; - the rollout of MCP 2.0, or &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/"&gt;the 2026-07-28 Model Context Protocol specification&lt;/a&gt; to use the more formal but less memorable name. This is the most significant change to the MCP spec since it first launched, and has also served to reignite my personal interest in the protocol.&lt;/p&gt;
&lt;p&gt;For background: MCP is the Model Context Protocol, which describes a standard way to expose new tools to LLM-powered agent frameworks. It was introduced by Anthropic back &lt;a href="https://www.anthropic.com/news/model-context-protocol"&gt;in November 2024&lt;/a&gt;, had a &lt;em&gt;huge&lt;/em&gt; spike of interest through much of 2025, and then became somewhat eclipsed by &lt;a href="https://simonwillison.net/2025/Oct/16/claude-skills/"&gt;Skills&lt;/a&gt; (another Anthropic invention) when it became apparent that an agent harness with access to a terminal and &lt;code&gt;curl&lt;/code&gt; could do most of what MCP did in a more flexible way. I wrote about that &lt;a href="https://simonwillison.net/2025/Dec/31/the-year-in-llms/#the-only-year-of-mcp"&gt;in my review of 2025&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I'm coming back around to MCP now. Giving an agent a shell environment with the ability to access the internet is &lt;a href="https://simonwillison.net/2026/Jul/22/openai-cyberattack/"&gt;fraught with risk&lt;/a&gt;, and requires a strong model that is capable of effectively driving such an environment. MCP tools are easier to audit and control, and simple enough that smaller models that run on a laptop can still drive them reasonably well.&lt;/p&gt;
&lt;p&gt;The new stateless MCP specification also greatly decreases the complexity of implementing both clients and servers for the protocol. I built three of those this week!&lt;/p&gt;
&lt;h4 id="what-s-easier-with-stateless-mcp"&gt;What's easier with stateless MCP&lt;/h4&gt;
&lt;p&gt;The best demonstration of the difference between stateful and stateless MCP is in this &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/"&gt;May 21st blog post&lt;/a&gt; that introduced the RC for the new specification. It included a clear before-and-after example.&lt;/p&gt;
&lt;p&gt;The older stateful MCP (I'm going to call it "legacy MCP") required two HTTP requests - the first to initialize a session and obtain a &lt;code&gt;Mcp-Session-Id&lt;/code&gt;, and the second to actually call the tool:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /mcp HTTP/1.1
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {
    },
    "clientInfo": {
      "name": "my-app",
      "version": "1.0"
    }
  }
}

POST /mcp HTTP/1.1
Mcp-Session-Id: 1868a90c-3a3f-4f5b
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {
      "q": "otters"
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new stateless way uses a single HTTP request which looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {
      "q": "otters"
    },
    "_meta": {
      "io.modelcontextprotocol/clientInfo": {
        "name": "my-app",
        "version": "1.0"
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is so much cleaner from both a client- and server-side implementation perspective. It's also a better fit for building scalable web applications, since now you don't need to maintain server-side state to keep track of those session IDs, or worry about routing the same session to the same backend machine.&lt;/p&gt;
&lt;h4 id="mcp-explorer"&gt;mcp-explorer&lt;/h4&gt;
&lt;p&gt;I couldn't find a great CLI tool for interactively probing an MCP server, so I had Codex help build my own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/mcp-explorer"&gt;mcp-explorer&lt;/a&gt;&lt;/strong&gt; is the result. It's a stateless Python CLI tool, so you don't even need to install it to try it out - it works with &lt;a href="https://docs.astral.sh/uv/guides/tools/#running-tools"&gt;uvx&lt;/a&gt; like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx mcp-explorer list https://agentic-mermaid.dev/mcp&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This queries Ade Oshineye's &lt;a href="https://agentic-mermaid.dev/"&gt;agentic-mermaid.dev&lt;/a&gt; demo MCP. The above command returns the following list of tools:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;execute(code: string, timeoutMs?: integer) - Execute Mermaid SDK code
  Run JavaScript in an isolated sandbox; return a value.

describe_sdk(family: string, detail?: string) - Describe Mermaid SDK operations
  Return version-matched mutation operations for one diagram family.

render_svg(source: string, options?: object) - Render Mermaid as SVG
  Render a Mermaid source string to themeable SVG. Returns { ok, svg }.

render_ascii(source: string, useAscii?: boolean, targetWidth?: integer, options?: object) - Render Mermaid as text
  Render a Mermaid source string to text. Returns { ok, text }.

render_png(source: string, scale?: number, background?: string, fitTo?: object, options?: object) - Render Mermaid as PNG
  Rasterize a Mermaid source string to PNG. Returns { ok, png_base64 }.
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then to inspect a tool:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx mcp-explorer inspect render_svg&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This outputs a whole bunch of information, including the JSON schema of the inputs and outputs.&lt;/p&gt;
&lt;p&gt;To call that tool and pass arguments to it:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx mcp-explorer call \
  https://agentic-mermaid.dev/mcp \
  render_svg \
  -a &lt;span class="pl-c1"&gt;source&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;graph TD; A--&amp;gt;B&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; \
  -a options &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;{"padding":24}&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Which returns:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{"ok":true,"svg":"&amp;lt;svg xmlns=\"http://www.w3.org/2000/svg\" width=...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To get just the raw SVG try adding &lt;code&gt;| jq .svg -r&lt;/code&gt; to that command. I got back &lt;a href="https://gist.github.com/simonw/b07c62f0ce103be6932477659d5dd1ac"&gt;this image&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/mermaid-example.svg" alt="SVG of as A box on top of a B box with an arrow from A to B" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;There are a &lt;a href="https://github.com/simonw/mcp-explorer/blob/main/README.md"&gt;few more commands&lt;/a&gt; in the README, but you get the general idea. I find building CLI tools like this to be a really productive way to get familiar with a specification, even if an agent writes most of the actual code.&lt;/p&gt;
&lt;h4 id="datasette-mcp"&gt;datasette-mcp&lt;/h4&gt;
&lt;p&gt;The second project is &lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-mcp"&gt;datasette-mcp&lt;/a&gt;&lt;/strong&gt;, a Datasette plugin which adds a &lt;code&gt;/-/mcp&lt;/code&gt; endpoint to any Datasette instance.&lt;/p&gt;
&lt;p&gt;This is probably the fourth time I've tried building this plugin, but thanks to the new stateless MCP specification I finally have a version that feels good to release.&lt;/p&gt;
&lt;p&gt;It provides just three tools: &lt;code&gt;list_databases()&lt;/code&gt;, &lt;code&gt;get_database_schema(database_name)&lt;/code&gt;, and &lt;code&gt;execute_sql(database_name, sql)&lt;/code&gt;. They do exactly what you would expect them to do - though &lt;code&gt;execute_sql()&lt;/code&gt; is read-only for the moment.&lt;/p&gt;
&lt;p&gt;Wire these into an agent, or a chat tool like ChatGPT or Claude, and they'll gain the ability to run SQL queries against your hosted Datasette instance.&lt;/p&gt;
&lt;p&gt;So far I'm running it on the Datasette mirror of my blog, at &lt;a href="datasette.simonwillison.net/-/mcp"&gt;datasette.simonwillison.net/-/mcp&lt;/a&gt;. It took a bit of fiddling to figure out how to attach that to ChatGPT and Claude, but I got there in the end. Here's &lt;a href="https://til.simonwillison.net/llms/mcp-in-claude-and-chatgpt"&gt;a new TIL&lt;/a&gt; showing exactly how to do that.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://claude.ai/share/de1ad9bf-f7c2-4fb9-a9a0-2a1ae39995db"&gt;a shared Claude session&lt;/a&gt; where I asked it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;list tables in simonwillison.net&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;what has Simon said recently about MCP?&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It ran 7 separate SQL queries to figure out the answer.&lt;/p&gt;
&lt;h4 id="llm-mcp-client"&gt;llm-mcp-client&lt;/h4&gt;
&lt;p&gt;My &lt;a href="https://llm.datasette.io/"&gt;LLM tool&lt;/a&gt; is long overdue for an official MCP integration. The new alpha &lt;a href="https://github.com/simonw/llm-mcp-client"&gt;llm-mcp-client&lt;/a&gt; plugin is my attempt at exactly that:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm install llm-mcp-client
llm -T &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;MCP("https://datasette.simonwillison.net/-/mcp")&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;count the notes&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here's the output (including reasoning trace, I'm using &lt;a href="https://simonwillison.net/2026/Jul/30/llm-rc2/"&gt;LLM 0.32rc2&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Considering note count&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I see the question "count the notes" is probably asking me to tally up blog notes. It could also mean published notes or drafts, so there's some ambiguity there. I'll need to figure out the total number of notes, likely by querying the count for both published notes and drafts to get a clear answer. Let's execute that count!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There are &lt;strong&gt;151 notes&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And &lt;a href="https://gist.github.com/simonw/4e8f558766150658ce35eab4f0fc3e04"&gt;the output of llm logs&lt;/a&gt; for that prompt.&lt;/p&gt;
&lt;p&gt;Once this is fully baked, I'm considering bringing it directly into LLM core. I'm excited to experiment with MCP in &lt;a href="https://agent.datasette.io/"&gt;Datasette Agent&lt;/a&gt; and &lt;a href="https://github.com/simonw/llm-coding-agent"&gt;llm-coding-agent&lt;/a&gt; as well.&lt;/p&gt;
&lt;h4 id="mcp-is-a-safer-way-to-build-with-agents"&gt;MCP is a safer way to build with agents&lt;/h4&gt;
&lt;p&gt;A few months after MCP was first released, I wrote &lt;a href="https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/"&gt;Model Context Protocol has prompt injection security problems&lt;/a&gt;, where I noted that the pattern of having end users mix and match tools pushed responsibility for avoiding data exfiltration attacks out to the users themselves. I hadn't coined &lt;a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/"&gt;the Lethal Trifecta&lt;/a&gt; yet, but that was absolutely what I had in mind.&lt;/p&gt;
&lt;p&gt;Then general agents with arbitrary shell and &lt;code&gt;curl&lt;/code&gt; access came along, and that's so much harder to keep secure!&lt;/p&gt;
&lt;p&gt;Something I've come to appreciate about MCP is that it's much easier to reason about agent capabilities and what might go wrong than with arbitrary command execution in an open network environment - the default for most of today's general and coding agent tools.&lt;/p&gt;
&lt;p&gt;I plan to lean into MCP a whole lot more when I'm building sensitive applications on top of LLMs.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mermaid"&gt;mermaid&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/model-context-protocol"&gt;model-context-protocol&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="ai"/><category term="datasette"/><category term="mermaid"/><category term="generative-ai"/><category term="llms"/><category term="llm"/><category term="anthropic"/><category term="model-context-protocol"/></entry><entry><title>datasette-agent 0.4a0</title><link href="https://simonwillison.net/2026/Jul/31/datasette-agent/" rel="alternate"/><published>2026-07-31T14:14:23+00:00</published><updated>2026-07-31T14:14:23+00:00</updated><id>https://simonwillison.net/2026/Jul/31/datasette-agent/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/datasette/datasette-agent/releases/tag/0.4a0"&gt;datasette-agent 0.4a0&lt;/a&gt;&lt;/p&gt;
        &lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;code&gt;await context.browser_task()&lt;/code&gt; mechanism allowing agent tools to run code directly in the user's browser. &lt;a href="https://github.com/datasette/datasette-agent/pull/33"&gt;#33&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is an exciting new capability: it makes it easy for Datasette Agent plugins to provide tools that execute custom JavaScript &lt;em&gt;in the user's browser&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I used this to add a debug loop to Datasette Apps in &lt;a href="https://simonwillison.net/2026/Aug/1/datasette-apps/"&gt;datasette-apps 0.2a0&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-tool-use"&gt;llm-tool-use&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-agent"&gt;datasette-agent&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="llm-tool-use"/><category term="datasette-agent"/></entry><entry><title>datasette-agent-modeler 0.1a0</title><link href="https://simonwillison.net/2026/Jul/29/datasette-agent-modeler/" rel="alternate"/><published>2026-07-29T02:47:05+00:00</published><updated>2026-07-29T02:47:05+00:00</updated><id>https://simonwillison.net/2026/Jul/29/datasette-agent-modeler/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/datasette/datasette-agent-modeler/releases/tag/0.1a0"&gt;datasette-agent-modeler 0.1a0&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;Bit of a wild vibe coding experiment with Claude Fable 5. I told it to "Build a plugin for datasette agent which adds a tool for creating a 3D model and then making edits to that model" (&lt;a href="https://github.com/datasette/datasette-agent-modeler/commit/a12b48a9626f6f74c3448b2a2dc0b606a091458f"&gt;full prompt here&lt;/a&gt;) and... it did.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a Datasette chat interface showing a 3D rendered cartoon orange cat with black whiskers, closed eyes, pink nose and curled tail sitting in a teal decorative pot with gold trim and handles on a grid floor. Prompt: put the cat in the pot. Text: Thinking: …will be cone shapes, and I'll add torus-shaped eyes along with a nose and smile., Tool: edit_model, Happy Cat Sitting in Decorative Pot · revision 3 · 50 objects · drag to orbit, scroll to zoom, right-drag to pan, Result: edit_model" src="https://static.simonwillison.net/static/2026/cat-in-a-pot.webp" /&gt;&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-agent"&gt;datasette-agent&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-mythos-fable"&gt;claude-mythos-fable&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="vibe-coding"/><category term="datasette-agent"/><category term="claude-mythos-fable"/></entry><entry><title>datasette-apps 0.1a4</title><link href="https://simonwillison.net/2026/Jul/14/datasette-apps/" rel="alternate"/><published>2026-07-14T23:44:56+00:00</published><updated>2026-07-14T23:44:56+00:00</updated><id>https://simonwillison.net/2026/Jul/14/datasette-apps/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/datasette/datasette-apps/releases/tag/0.1a4"&gt;datasette-apps 0.1a4&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-apps"&gt;datasette-apps&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="datasette-apps"/></entry><entry><title>datasette 1.0a37</title><link href="https://simonwillison.net/2026/Jul/14/datasette/" rel="alternate"/><published>2026-07-14T16:31:41+00:00</published><updated>2026-07-14T16:31:41+00:00</updated><id>https://simonwillison.net/2026/Jul/14/datasette/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a37"&gt;datasette 1.0a37&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;A minor release. Performance and &lt;a href="https://docs.datasette.io/en/latest/authentication.html#authentication-permissions-explained"&gt;documentation&lt;/a&gt; improvements to the permissions system, plus I reverted a cosmetic API change which caused almost every existing plugin test suite to break.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/></entry><entry><title>DOOMQL</title><link href="https://simonwillison.net/2026/Jul/13/doomql/" rel="alternate"/><published>2026-07-13T22:34:41+00:00</published><updated>2026-07-13T22:34:41+00:00</updated><id>https://simonwillison.net/2026/Jul/13/doomql/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/petergpt/doomql"&gt;DOOMQL&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Peter Gostev built this using GPT-5.6 Sol. This is a &lt;em&gt;lot&lt;/em&gt; of fun: &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DOOMQL started with a deliberately unreasonable question: what if SQLite were the game engine, not merely the place where a game stores data?&lt;/p&gt;
&lt;p&gt;The result is a small, original Doom-like game in which SQL owns movement, collision, enemies, combat, progression and every RGB pixel on screen.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's implemented as a Python terminal script - I tried it out like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /tmp
git clone https://github.com/petergpt/doomql
cd doomql
uv run host/doomql.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img alt="Screenshot of a macOS terminal window titled &amp;quot;doomql — python3.14 ◂ uv run host/doomql.py — 134×31&amp;quot; showing a retro Doom-style game rendered as text-mode pixel art. The scene is a pixelated first-person corridor with gray paneled walls, dark red doors on the far left and right, a floating cyan-and-gold coin pickup on the right side, a white crosshair near the center, and a dark weapon barrel rising from the bottom center. A status bar below the scene reads &amp;quot;HP 100/100 AMMO 037 SCORE 00225 INDEX MISSING TICK 0028450&amp;quot;, followed by an orange line &amp;quot;FIND THE INDEX TOKEN&amp;quot; and a cyan controls line &amp;quot;WASD MOVE J/L OR ARROWS TURN SPACE FIRE E USE P PAUSE CTRL-C EXIT&amp;quot;." src="https://static.simonwillison.net/static/2026/doomql-window.png" /&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/petergpt/doomql/blob/main/sql/003_render.sql"&gt;the huge SQL query&lt;/a&gt; that implements a full ray tracer in SQLite using a recursive CTE.&lt;/p&gt;
&lt;p&gt;Running the above script creates a &lt;code&gt;/tmp/doomql/.doomql/doomql.sqlite&lt;/code&gt; SQLite database, which you can explore using Datasette like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx --prerelease=allow  --with datasette-apps datasette \
  /tmp/doomql/.doomql/doomql.sqlite \
  -p 4444 --root --secret 1 --internal internal.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--with datasette-apps&lt;/code&gt; option installs the new &lt;a href="https://simonwillison.net/2026/Jun/18/datasette-apps/"&gt;Datasette Apps&lt;/a&gt; plugin, which supports creating custom HTML+JavaScript apps that can run SQL queries directly within the Datasette interface.&lt;/p&gt;
&lt;p&gt;I created a new app, pasted the copy-paste prompt into Claude chat (Fable 5) &lt;a href="https://claude.ai/share/c793280c-2ef1-4555-a7c2-31281abfdf78"&gt;and told it&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build an app that displays the current state of the screen using the frame_pixels view with its x, y, r, g, b columns. have it refresh once a second.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This got me a working HTML+JavaScript app inside Datasette that could reflect the current state while I played the game in my terminal. Then I added:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;add a minimap&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And now my Datasette App looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a dark-themed web app running a retro Doom-style game rendered from SQL queries. The page header reads &amp;quot;DOOMQL&amp;quot; with buttons &amp;quot;All apps&amp;quot;, &amp;quot;Edit app&amp;quot;, &amp;quot;Pin&amp;quot;, and &amp;quot;Full screen&amp;quot;. Inside the game panel, the title &amp;quot;DOOMQL&amp;quot; sits above the subtitle &amp;quot;auto-refreshing once a second · frame and tactical map straight from SQL&amp;quot;. The left side shows a pixelated first-person corridor view with gray walls, dark red doors, a floating cyan-and-gold coin pickup, a white crosshair, and a weapon barrel at bottom center. A status bar below reads &amp;quot;HP 100/100 AMMO 037 SCORE 00225 INDEX MISSING TICK 0027847&amp;quot;. On the right, a panel titled &amp;quot;TACTICAL MAP&amp;quot; shows a top-down grid map with a player triangle, a red enemy circle, yellow pickup dots, red wall markers, and a green exit square, with a legend reading &amp;quot;you&amp;quot;, &amp;quot;enemy&amp;quot;, &amp;quot;pickup&amp;quot;, &amp;quot;locked door&amp;quot;, &amp;quot;door&amp;quot;, &amp;quot;exit&amp;quot;. Below the game view, an orange banner reads &amp;quot;FIND THE INDEX TOKEN&amp;quot;, followed by the cyan line &amp;quot;READ-ONLY VIEWER · SELECT x, y, r, g, b FROM frame_pixels&amp;quot;. At the bottom, a green &amp;quot;RUNNING&amp;quot; badge appears beside the stats &amp;quot;160×54 · 8,640 pixels · 3 hostiles · query 89 ms · refreshing every 1 s&amp;quot;." src="https://static.simonwillison.net/static/2026/doomql-datasette-app.png" /&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/7c78184476fccd4b70b02f7f9048dffa"&gt;the HTML app code&lt;/a&gt; - paste that into your own Datasette instance (using the &lt;code&gt;uvx --with datasette-apps&lt;/code&gt; recipe from above) to try it yourself.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/petergostev/status/2076692164310884468"&gt;@petergostev&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/games"&gt;games&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gpt"&gt;gpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-apps"&gt;datasette-apps&lt;/a&gt;&lt;/p&gt;



</summary><category term="games"/><category term="sql"/><category term="sqlite"/><category term="ai"/><category term="datasette"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="gpt"/><category term="datasette-apps"/></entry><entry><title>datasette code-frequency chart on GitHub</title><link href="https://simonwillison.net/2026/Jul/13/datasette-code-frequency/" rel="alternate"/><published>2026-07-13T21:45:27+00:00</published><updated>2026-07-13T21:45:27+00:00</updated><id>https://simonwillison.net/2026/Jul/13/datasette-code-frequency/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette/graphs/code-frequency"&gt;datasette code-frequency chart on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Out of curiosity I decided to see if I could find a useful illustration of the impact of coding agents and Opus 4.5 class models on my own output. The best I've found so far is this GitHub chart of frequency of code changes to my &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; open source project:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a GitHub &amp;quot;Code frequency&amp;quot; bar chart, subtitled &amp;quot;Additions and deletions per week&amp;quot;, showing green addition bars and red deletion bars per week from 2018 through 2026, with a y-axis labeled Frequency ranging from -20k to 30k. Activity comes in sporadic bursts: the largest spike is 37,022 additions with -9,528 deletions in 2026, followed by 14,638 additions with -6,584 deletions in late 2025, 15,998 additions in early 2018, and a standout deletion spike of -10,658 in mid-2020, with quieter periods of smaller weekly changes in between." src="https://static.simonwillison.net/static/2026/datasette-code-frequency.png" /&gt;&lt;/p&gt;
&lt;p&gt;The big spike in activity at the end aligns with Opus 4.8, GPT-5.5, Fable 5 and GPT-5.6 Sol.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;&lt;/p&gt;



</summary><category term="github"/><category term="ai"/><category term="datasette"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="coding-agents"/></entry><entry><title>datasette 1.0a36</title><link href="https://simonwillison.net/2026/Jul/7/datasette/" rel="alternate"/><published>2026-07-07T21:43:27+00:00</published><updated>2026-07-07T21:43:27+00:00</updated><id>https://simonwillison.net/2026/Jul/7/datasette/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a36"&gt;datasette 1.0a36&lt;/a&gt;&lt;/p&gt;
        &lt;blockquote&gt;
&lt;p&gt;The signature features of this alpha are new UIs for &lt;strong&gt;inserting multiple rows at once&lt;/strong&gt; (from TSV, CSV or JSON) and for &lt;strong&gt;creating a table from rows&lt;/strong&gt;, plus a large number of small &lt;strong&gt;JSON API consistency fixes&lt;/strong&gt; in preparation for a 1.0 stable release.&lt;/p&gt;
&lt;/blockquote&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/></entry><entry><title>Using DSPy to evaluate and improve Datasette Agent's SQL system prompts</title><link href="https://simonwillison.net/2026/Jul/2/dspy-datasette-agent-prompts/" rel="alternate"/><published>2026-07-02T18:25:00+00:00</published><updated>2026-07-02T18:25:00+00:00</updated><id>https://simonwillison.net/2026/Jul/2/dspy-datasette-agent-prompts/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Research:&lt;/strong&gt; &lt;a href="https://github.com/simonw/research/tree/main/dspy-datasette-agent-prompts#readme"&gt;Using DSPy to evaluate and improve Datasette Agent&amp;#x27;s SQL system prompts&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;One of this morning's AIE keynotes covered &lt;a href="https://github.com/stanfordnlp/dspy"&gt;dspy&lt;/a&gt;, which reminded me I've been meaning to see if it could help me improve the system prompt used by &lt;a href="https://agent.datasette.io"&gt;Datasette Agent&lt;/a&gt; - so I fired off an asynchronous research task in Claude Code for web using Claude Fable 5:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Pip install the latest Datasette alpha and datasette-agent and dspy - then figure out how to use dspy to evaluate and improve the main system prompts used by Datasette Agent for the feature where it can execute read only SQL queries to answer user questions about data.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Fable chose to test using GPT 4.1 mini and nano, and identified several promising looking directions for improvements. I particularly like this one:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The schema listing gives only table names; the "don't call describe_table if you already have the information" advice caused column-name guessing (page_count, o.order_id, first_name) and error-retry loops in baseline traces. Either include column names in the prompt's schema listing or soften that advice.&lt;/p&gt;
&lt;/blockquote&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/evals"&gt;evals&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dspy"&gt;dspy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-agent"&gt;datasette-agent&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-mythos-fable"&gt;claude-mythos-fable&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="ai"/><category term="datasette"/><category term="generative-ai"/><category term="llms"/><category term="evals"/><category term="dspy"/><category term="datasette-agent"/><category term="claude-mythos-fable"/></entry><entry><title>Have your agent record video demos of its work with shot-scraper video</title><link href="https://simonwillison.net/2026/Jun/30/shot-scraper-video/" rel="alternate"/><published>2026-06-30T16:54:26+00:00</published><updated>2026-06-30T16:54:26+00:00</updated><id>https://simonwillison.net/2026/Jun/30/shot-scraper-video/</id><summary type="html">
    &lt;p&gt;&lt;a href="https://shot-scraper.datasette.io/en/stable/video.html"&gt;shot-scraper video&lt;/a&gt; is a new command introduced in today's &lt;a href="https://github.com/simonw/shot-scraper/releases/tag/1.10"&gt;shot-scraper 1.10&lt;/a&gt; release which accepts a &lt;code&gt;storyboard.yml&lt;/code&gt; file defining a routine to run against a web application and uses Playwright to record a video of that routine. I've written before about the importance of &lt;a href="https://simonwillison.net/2026/Feb/10/showboat-and-rodney/#proving-code-actually-works"&gt;having coding agents produce demos&lt;/a&gt; of their work; this is my latest attempt at enabling them to do that.&lt;/p&gt;
&lt;p&gt;Here's an example video created using &lt;code&gt;shot-scraper video&lt;/code&gt;, exercising a &lt;a href="https://github.com/simonw/datasette/pull/2813"&gt;still in development&lt;/a&gt; feature adding the ability to create new tables in Datasette from pasted CSV, TSV or JSON data:&lt;/p&gt;
&lt;div style="max-width: 100%; margin-bottom: 0.4em"&gt;
    &lt;video controls="controls" preload="none" aria-label="Video demo of the new CSV import for Datasette" poster="https://static.simonwillison.net/static/2026/datasette-bulk-insert-demo.jpg" loop="loop" style="width: 100%; height: auto;" muted="muted"&gt;
        &lt;source src="https://static.simonwillison.net/static/2026/datasette-bulk-insert-demo.mp4" type="video/mp4" /&gt;
    &lt;/video&gt;
&lt;/div&gt;
&lt;p&gt;That video was created by running this command:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;shot-scraper video datasette-bulk-insert-storyboard.yml \
  --auth datasette-demo-auth.json --mp4&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(That &lt;code&gt;--auth&lt;/code&gt; JSON file &lt;a href="https://gist.github.com/simonw/287b26aff53fcb72942b19f5b69d7e5c"&gt;contains a cookie&lt;/a&gt;, as &lt;a href="https://shot-scraper.datasette.io/en/stable/authentication.html"&gt;described here&lt;/a&gt; in the documentation.)&lt;/p&gt;
&lt;p&gt;Here's the &lt;code&gt;datasette-bulk-insert-storyboard.yml&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;output&lt;/span&gt;: &lt;span class="pl-s"&gt;/tmp/datasette-bulk-insert-demo.webm&lt;/span&gt;
&lt;span class="pl-ent"&gt;server&lt;/span&gt;:
  - &lt;span class="pl-s"&gt;uv&lt;/span&gt;
  - &lt;span class="pl-s"&gt;--directory&lt;/span&gt;
  - &lt;span class="pl-s"&gt;/Users/simon/Dropbox/dev/datasette&lt;/span&gt;
  - &lt;span class="pl-s"&gt;run&lt;/span&gt;
  - &lt;span class="pl-s"&gt;datasette&lt;/span&gt;
  - &lt;span class="pl-s"&gt;-p&lt;/span&gt;
  - &lt;span class="pl-c1"&gt;6419&lt;/span&gt;
  - &lt;span class="pl-s"&gt;--root&lt;/span&gt;
  - &lt;span class="pl-s"&gt;--secret&lt;/span&gt;
  - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  - &lt;span class="pl-s"&gt;/tmp/demo.db&lt;/span&gt;
&lt;span class="pl-ent"&gt;url&lt;/span&gt;: &lt;span class="pl-s"&gt;http://127.0.0.1:6419/demo/tasks&lt;/span&gt;
&lt;span class="pl-ent"&gt;viewport&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;width&lt;/span&gt;: &lt;span class="pl-c1"&gt;1280&lt;/span&gt;
  &lt;span class="pl-ent"&gt;height&lt;/span&gt;: &lt;span class="pl-c1"&gt;720&lt;/span&gt;
&lt;span class="pl-ent"&gt;cursor&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
&lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;button[data-table-action="insert-row"]&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-ent"&gt;javascript&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;  (() =&amp;gt; {&lt;/span&gt;
&lt;span class="pl-s"&gt;    let clipboardText = "";&lt;/span&gt;
&lt;span class="pl-s"&gt;    Object.defineProperty(navigator, "clipboard", {&lt;/span&gt;
&lt;span class="pl-s"&gt;      configurable: true,&lt;/span&gt;
&lt;span class="pl-s"&gt;      get: () =&amp;gt; ({&lt;/span&gt;
&lt;span class="pl-s"&gt;        writeText: async (text) =&amp;gt; {&lt;/span&gt;
&lt;span class="pl-s"&gt;          clipboardText = String(text);&lt;/span&gt;
&lt;span class="pl-s"&gt;        },&lt;/span&gt;
&lt;span class="pl-s"&gt;        readText: async () =&amp;gt; clipboardText,&lt;/span&gt;
&lt;span class="pl-s"&gt;      }),&lt;/span&gt;
&lt;span class="pl-s"&gt;    });&lt;/span&gt;
&lt;span class="pl-s"&gt;  })();&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;&lt;span class="pl-ent"&gt;scenes&lt;/span&gt;:
  - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Bulk insert existing table rows&lt;/span&gt;
    &lt;span class="pl-ent"&gt;do&lt;/span&gt;:
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.8&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;button[data-table-action="insert-row"]&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#row-edit-dialog[open]&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.5&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.row-edit-bulk-insert&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.row-edit-bulk-textarea&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.5&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.row-edit-copy-template&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;text=Copied&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.8&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;fill&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;into&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.row-edit-bulk-textarea&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
          &lt;span class="pl-ent"&gt;text&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;            title,owner,status,priority,notes&lt;/span&gt;
&lt;span class="pl-s"&gt;            Prepare release video,Ana,doing,1,Recorded with shot-scraper&lt;/span&gt;
&lt;span class="pl-s"&gt;            Check pasted CSV import,Ben,review,3,Previewed before inserting&lt;/span&gt;
&lt;span class="pl-s"&gt;            Share the branch demo,Chen,queued,2,Bulk insert creates three rows&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.8&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.row-edit-save&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;text=Previewing 3 rows.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;1.2&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.row-edit-save&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;text=3 rows inserted.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;1.0&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.row-edit-cancel&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;text=Prepare release video&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;1.0&lt;/span&gt;
  - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Create a table from pasted CSV&lt;/span&gt;
    &lt;span class="pl-ent"&gt;open&lt;/span&gt;: &lt;span class="pl-s"&gt;http://127.0.0.1:6419/demo&lt;/span&gt;
    &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;details.actions-menu-links summary&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-ent"&gt;do&lt;/span&gt;:
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.8&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;details.actions-menu-links summary&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;button[data-database-action="create-table"]&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#table-create-dialog[open]&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.5&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;fill&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;into&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.table-create-table-name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
          &lt;span class="pl-ent"&gt;text&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;launch_metrics&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.table-create-from-data&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.table-create-data-textarea&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.5&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;fill&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;into&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.table-create-data-textarea&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
          &lt;span class="pl-ent"&gt;text&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;            metric_id,name,score,recorded_on&lt;/span&gt;
&lt;span class="pl-s"&gt;            m001,Activation rate,87.5,2026-06-29&lt;/span&gt;
&lt;span class="pl-s"&gt;            m002,Retention check,72.25,2026-06-30&lt;/span&gt;
&lt;span class="pl-s"&gt;            m003,CSV import health,95,2026-07-01&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.8&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.table-create-save&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;text=Previewing 3 rows.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;1.2&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;click&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.table-create-save&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for_url&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;**/demo/launch_metrics&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;wait_for&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;text=Activation rate&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-ent"&gt;pause&lt;/span&gt;: &lt;span class="pl-c1"&gt;1.2&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;a href="https://shot-scraper.datasette.io/en/stable/video.html"&gt;video command documentation&lt;/a&gt; includes simpler examples, but for the purpose of this post I thought I'd go with something more comprehensive.&lt;/p&gt;
&lt;p&gt;That demo YAML storyboard was constructed entirely by GPT-5.5 xhigh running in Codex Desktop, using the following prompt run inside my &lt;code&gt;~/dev/datasette&lt;/code&gt; checkout of &lt;a href="https://github.com/simonw/datasette/commits/b759ea548606bc9bf9a4bf0e33e2d57ead7e0ab8/"&gt;this branch&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Review the changes on this branch.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cd to ~/dev/shot-scraper and run the command "uv run shot-scraper video --help"&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Now use that new video command to record a video demo of the new features from this branch, including running a "uv run datasette -p 6419 --root --secret 1 /tmp/demo.db" development server so you can record the video against a demo DB that you first create.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now that I've released the feature the prompt could say "&lt;code&gt;run uvx shot-scraper video --help&lt;/code&gt;" instead and it should achieve the same result.&lt;/p&gt;
&lt;p&gt;I really like this pattern where the &lt;code&gt;--help&lt;/code&gt; output for a command provides enough detail that a coding agent can use it - it works kind of like bundling a &lt;code&gt;SKILL.md&lt;/code&gt; file directly inside the tool. I used the same pattern for &lt;a href="https://simonwillison.net/2026/Feb/10/showboat-and-rodney/"&gt;showboat and rodney&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="how-i-built-this"&gt;How I built this&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;shot-scraper video&lt;/code&gt; started as an experimental prototype. &lt;code&gt;shot-scraper&lt;/code&gt; is built on top of &lt;a href="https://playwright.dev/"&gt;Playwright&lt;/a&gt;, and the key feature it needed was for Playwright to be able to record video of browser sessions with enough control to create the desired demo.&lt;/p&gt;
&lt;p&gt;I first tried this a few years ago and found that the Playwright-produced videos included additional chrome that was useful for debugging a test failure but unwanted for a product demo.&lt;/p&gt;
&lt;p&gt;They fixed that a while ago, but there were still some minor blockers. In particular I was getting &lt;a href="https://github.com/simonw/shot-scraper/pull/194/changes/c2f3b3a52ba84f2adcf3ad6da4d39c2570328584#issuecomment-4724459369"&gt;a few white frames at the start of the videos&lt;/a&gt;, since the recording mechanism kicked in before the first URL was loaded by the browser.&lt;/p&gt;
&lt;p&gt;Playwright 1.59 added a new &lt;a href="https://playwright.dev/python/docs/api/class-screencast"&gt;screencast mechanism&lt;/a&gt; providing much more finely grained control over video recording. This was very nearly what I needed, but the resulting videos were fixed at 800px wide.&lt;/p&gt;
&lt;p&gt;I found a &lt;a href="https://github.com/microsoft/playwright/pull/41183"&gt;landed PR fixing that&lt;/a&gt; but it wasn't yet in a release. Then yesterday they shipped it in &lt;a href="https://github.com/microsoft/playwright-python/releases/tag/v1.61.0"&gt;playwright-python 1.61.0&lt;/a&gt; and I was finally unblocked to finish implementing the feature!&lt;/p&gt;
&lt;p&gt;The code itself was all written by GPT-5.5 xhigh in Codex Desktop. I had it write the documentation as well which gave me a very useful frame for reviewing the design - much of the iteration on the feature came from reviewing that documentation, spotting things that were redundant, inconsistent or confusing, and requesting (or dictating) a better design.&lt;/p&gt;
&lt;p&gt;The YAML format itself was mostly defined by the coding agent. I had it &lt;a href="https://github.com/simonw/shot-scraper/blob/1.10/shot_scraper/video.py#L24"&gt;use Pydantic&lt;/a&gt; to both define and validate the format, partly to make the design easier to review.&lt;/p&gt;
&lt;p&gt;This is a great example of the kind of feature that I almost certainly wouldn't have taken on without coding agent support. I filed the &lt;a href="https://github.com/simonw/shot-scraper/issues/142"&gt;original issue&lt;/a&gt; in February 2024, and had difficulty finding the necessary time to solve this in amongst all of my other projects.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yaml"&gt;yaml&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/playwright"&gt;playwright&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shot-scraper"&gt;shot-scraper&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pydantic"&gt;pydantic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/agentic-engineering"&gt;agentic-engineering&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="python"/><category term="yaml"/><category term="ai"/><category term="datasette"/><category term="playwright"/><category term="shot-scraper"/><category term="generative-ai"/><category term="llms"/><category term="pydantic"/><category term="coding-agents"/><category term="agentic-engineering"/></entry><entry><title>datasette-export-database 0.3a2</title><link href="https://simonwillison.net/2026/Jun/25/datasette-export-database/" rel="alternate"/><published>2026-06-25T17:21:09+00:00</published><updated>2026-06-25T17:21:09+00:00</updated><id>https://simonwillison.net/2026/Jun/25/datasette-export-database/</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/datasette/datasette-export-database/releases/tag/0.3a2"&gt;datasette-export-database 0.3a2&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;An embarrassingly tiny release. The &lt;code&gt;pyproject.toml&lt;/code&gt; had pinned to &lt;code&gt;datasette==1.0a27&lt;/code&gt;, inadvertently making this plugin incompatible with all other Datasette versions. It's now &lt;code&gt;datasette&amp;gt;=1.0a27&lt;/code&gt; instead.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/></entry></feed>