vietnam crab exportersoft-shell crab

Simon Willison’s Weblog

Subscribe
Atom feed

Elsewhere

Filters: Sorted by date

The datasette.io website has a news section built from this news.yaml file in the underlying GitHub repository. The YAML format looks like this:

- date: 2026-04-15
  body: |-
    [Datasette 1.0a27](https://docs.datasette.io/en/latest/changelog.html#a27-2026-04-15) changes how CSRF protection works in a way that simplifies form and API integration, and introduces a new `RenameTableEvent` for when a table is renamed by a SQL query.
- date: 2026-03-18
  body: |-
    ...

This format is a little hard to edit, so I finally had Claude build a custom preview UI to make checking for errors have slightly less friction.

I built it using standard claude.ai and Claude Artifacts, taking advantage of Claude's ability to clone GitHub repos and look at their content as part of a regular chat:

Clone https://github.com/simonw/datasette.io and look at the news.yaml file and how it is rendered on the homepage. Build an artifact I can paste that YAML into which previews what it will look like, and highlights any markdown errors or YAML errors

Screenshot showing two side-by-side views of a datasette.io news preview tool. The left panel shows a dark-themed YAML editor with news entries containing date and body fields in Markdown format, with a red validation error at the bottom indicating the date field has an invalid format. The right panel shows the rendered preview output with formatted headings by date (April 2026, 18th March 2026), displaying 115 news entries with linked release names, inline code snippets, and changelog descriptions. A red badge with "1" appears on the left panel header indicating one validation error.

Release datasette-export-database 0.3a1 — Export a copy of a mutable SQLite database on demand

This plugin was using the ds_csrftoken cookie as part of a custom signed URL, which needed upgrading now that Datasette 1.0a27 no longer sets that cookie.

Release datasette 1.0a27 — An open source multi-tool for exploring and publishing data

Two major changes in this new Datasette alpha. I covered the first of those in detail yesterday - Datasette no longer uses Django-style CSRF form tokens, instead using modern browser headers as described by Filippo Valsorda.

The second big change is that Datasette now fires a new RenameTableEvent any time a table is renamed during a SQLite transaction. This is useful because some plugins (like datasette-comments) attach additional data to table records by name, so a renamed table requires them to react in appropriate ways.

Here are the rest of the changes in the alpha:

  • New actor= parameter for datasette.client methods, allowing internal requests to be made as a specific actor. This is particularly useful for writing automated tests. (#2688)
  • New Database(is_temp_disk=True) option, used internally for the internal database. This helps resolve intermittent database locked errors caused by the internal database being in-memory as opposed to on-disk. (#2683) (#2684)
  • The /<database>/<table>/-/upsert API (docs) now rejects rows with null primary key values. (#1936)
  • Improved example in the API explorer for the /-/upsert endpoint (docs). (#1936)
  • The /<database>.json endpoint now includes an "ok": true key, for consistency with other JSON API responses.
  • call_with_supported_arguments() is now documented as a supported public API. (#2678)
Tool Gemini 3.1 Flash TTS — Convert text to natural-sounding speech using Google's Gemini 3.1 Flash TTS model with support for both single-speaker and multi-speaker conversation modes. The tool allows you to customize voice selection, apply directorial tags like `[whisper]` and `[short pause]` for dynamic delivery, and download the generated audio as a WAV file. Requires a valid Gemini API key to function.

See my notes on Google's new Gemini 3.1 Flash TTS text-to-speech model.

Release datasette-ports 0.3 — Find all currently running Datasette instances and list their ports

A small update for my tool for helping me figure out what all of the Datasette instances on my laptop are up to.

  • Show working directory derived from each PID
  • Show the full path to each database file

Output now looks like this:

http://127.0.0.1:8007/ - v1.0a26
  Directory: /Users/simon/dev/blog
  Databases:
    simonwillisonblog: /Users/simon/dev/blog/simonwillisonblog.db
  Plugins:
    datasette-llm
    datasette-secrets
http://127.0.0.1:8001/ - v1.0a26
  Directory: /Users/simon/dev/creatures
  Databases:
    creatures: /tmp/creatures.db
Research Exploring the new `servo` crate — After the April 2026 release of the `servo` v0.1.0 crate (blog post), a concise investigation shows that Servo is now an embeddable browser engine for Rust, with a clear API centered on the `ServoBuilder`, `WebView`, and pixel readback methods. A headless CLI (`servo-shot`) successfully renders URLs or HTML files to PNG, building against stable Rust with a robust software-based rendering pipeline.

In Servo is now available on crates.io the Servo team announced the initial release of the servo crate, which packages their browser engine as an embeddable library.

I set Claude Code for web the task of figuring out what it can do, building a CLI tool for taking screenshots using it and working out if it could be compiled to WebAssembly.

The servo-shot Rust tool it built works pretty well:

git clone https://github.com/simonw/research
cd research/servo-crate-exploration/servo-shot
cargo build
./target/debug/servo-shot https://news.ycombinator.com/

Here's the result:

An accurately rendered screenshot of the Hacker News homepage

Compiling Servo itself to WebAssembly is not feasible due to its heavy use of threads and dependencies like SpiderMonkey, but Claude did build me this playground page for trying out a WebAssembly build of the html5ever and markup5ever_rcdom crates, providing a tool for turning fragments of HTML into a parse tree.

Research QuickJS Python Sandbox — Investigation Report — Exploring the `quickjs` Python package, this project implements an asyncio-compatible JavaScript sandbox with robust resource controls and seamless exposure of both synchronous and asynchronous Python functions (including async httpx fetches) to JavaScript code.
Tool SQLite Query Result Formatter Demo — Format SQLite query results in 20 different styles including box-drawing tables, CSV, JSON, HTML, Markdown, and more using this interactive WebAssembly-based demonstration. Adjust formatting options like column headers, screen width, NULL display values, and border styles in real-time to see how your SQL queries render across all available output modes. The demo database includes sample tables for employees, products, and orders with pre-built example queries showcasing each formatting style.

See my notes on SQLite 3.53.0. This playground provides a UI for trying out the various rendering options for SQL result tables from the new Query Result Formatter library, compiled to WebAssembly.

Tool GitHub Repo Size — Check the size of any GitHub repository by entering the owner and repository name or pasting a GitHub URL. The tool fetches repository data from the GitHub API and displays the total size in kilobytes, megabytes, or gigabytes depending on the repository's scale. Results are automatically saved to the browser URL, allowing you to share or revisit repository size checks.

GitHub doesn't tell you the repo size in the UI, but it's available in the CORS-friendly API. Paste a repo into this tool to see the size, for example for simonw/datasette (8.1MB).

Release datasette-gzip 0.3 — Add gzip compression to Datasette
Release asgi-gzip 0.3 — gzip middleware for ASGI applications, extracted from Starlette

I ran into trouble deploying a new feature using SSE to a production Datasette instance, and it turned out that instance was using datasette-gzip which uses asgi-gzip which was incorrectly compressing event/text-stream responses.

asgi-gzip was extracted from Starlette, and has a GitHub Actions scheduled workflow to check Starlette for updates that need to be ported to the library... but that action had stopped running and hence had missed Starlette's own fix for this issue.

I ran the workflow and integrated the new fix, and now datasette-gzip and asgi-gzip both correctly handle text/event-stream in SSE responses.

Release datasette-turnstile 0.1a3 — Configurable CAPTCHAs for Datasette paths using Cloudflare Turnstile
Release datasette-graphql 3.0a1 — Datasette plugin providing an automatic GraphQL API for your SQLite databases
Release datasette-atom 0.10a0 — Datasette plugin that adds a .atom output format
Release dogsheep-beta 0.11 — Build a search index across content from multiple SQLite database tables and run faceted searches against it using Datasette
Release datasette-template-sql 1.0.3 — Datasette plugin for executing SQL queries from templates
Release datasette-turnstile 0.1a2 — Configurable CAPTCHAs for Datasette paths using Cloudflare Turnstile
Release datasette-turnstile 0.1a1 — Configurable CAPTCHAs for Datasette paths using Cloudflare Turnstile
Research SQLite WAL Mode Across Docker Containers Sharing a Volume — SQLite’s WAL mode reliably supports concurrent access when two Docker containers share a volume on the same host, due to shared kernel and filesystem semantics. The experiment, using Docker Desktop for macOS and a named volume, demonstrated real-time propagation of database changes and effective memory-mapped file sharing by monitoring `.db-shm`.

Inspired by this conversation on Hacker News about whether two SQLite processes in separate Docker containers that share the same volume might run into problems due to WAL shared memory. The answer is that everything works fine - Docker containers on the same host and filesystem share the same shared memory in a way that allows WAL to collaborate as it should.

Release datasette-ports 0.2 — Find all currently running Datasette instances and list their ports
  • No longer requires Datasette - running uvx datasette-ports now works as well.
  • Installing it as a Datasette plugin continues to provide the datasette ports command.
Release scan-for-secrets 0.3 — Scan for secrets in files you plan to share
  • New -r/--redact option which shows the list of matches, asks for confirmation and then replaces every match with REDACTED, taking escaping rules into account.
  • New Python function redact_file(file_path: str | Path, secrets: list[str], replacement: str = "REDACTED") -> int.
Tool Cleanup Claude Code Paste — Clean up Claude Code terminal output by removing the ❯ prompt character, fixing whitespace from line wrapping, and joining broken lines into readable paragraphs. Paste your terminal text into the input field and the cleaned output will appear automatically, ready to copy to your clipboard.

Super-niche tool this. I sometimes copy prompts out of the Claude Code terminal app and they come out with a bunch of weird additional whitespace. This tool cleans that up.

Screenshot of a web tool titled "Cleanup Claude Code Paste" with the subtitle "Paste terminal output to remove the ❯ prompt, fix wrapped-line whitespace, and join lines into clean text." An input textarea contains pasted terminal output starting with "❯ Add a -r/--redact option which asks for user approval (after telling it how many replacements will happen and in which files and which lines – standard output basically) and then rewrites the files in that folder to replace all matched secrets with REDACTED. Run tests with 'uv run pytest' and use red/green TDD". Below is a "Cleaned output:" section showing the same text with the ❯ prompt removed and whitespace cleaned up. A blue "Copy to clipboard" button appears at the bottom.

Release datasette-ports 0.1 — Find all currently running Datasette instances and list their ports

Another example of README-driven development, this time solving a problem that might be unique to me.

I often find myself running a bunch of different Datasette instances with different databases and different in-development plugins, spreads across dozens of different terminal windows - enough that I frequently lose them!

Now I can run this:

datasette install datasette-ports
datasette ports

And get a list of every running instance that looks something like this:

http://127.0.0.1:8333/ - v1.0a26
  Databases: data
  Plugins: datasette-enrichments, datasette-enrichments-llm, datasette-llm, datasette-secrets
http://127.0.0.1:8001/ - v1.0a26
  Databases: creatures
  Plugins: datasette-extract, datasette-llm, datasette-secrets
http://127.0.0.1:8900/ - v0.65.2
  Databases: logs
Tool Syntaqlite Playground — # Syntaqlite Playground

Lalit Maganti's syntaqlite is currently being discussed on Hacker News thanks to Eight years of wanting, three months of building with AI, a deep dive into how it was built.

This inspired me to revisit a research project I ran when Lalit first released it a couple of weeks ago, where I tried it out and then compiled it to a WebAssembly wheel so it could run in Pyodide in a browser (the library itself uses C and Rust).

This new playground loads up the Python library and provides a UI for trying out its different features: formating, parsing into an AST, validating, and tokenizing SQLite SQL queries.

Screenshot of a dark-themed SQL validation playground called SyntaqLite. The "Validate" tab is selected from options including Format, Parse, Validate, and Tokenize. The SQL input contains "SELECT id, name FROM usr WHERE active = 1" with a schema defining "users" and "posts" tables. Example buttons for "Table typo", "Column typo", and "Valid query" are shown above a red "Validate SQL" button. The Diagnostics panel shows an error for unknown table 'usr' with the suggestion "did you mean 'users'?", and the JSON panel displays the corresponding error object with severity, message, and offset fields.

Update: not sure how I missed this but syntaqlite has its own WebAssembly playground linked to from the README.

Release scan-for-secrets 0.2 — Scan for secrets in files you plan to share
  • CLI tool now streams results as they are found rather than waiting until the end, which is better for large directories.
  • -d/--directory option can now be used multiple times to scan multiple directories.
  • New -f/--file option for specifying one or more individual files to scan.
  • New scan_directory_iter(), scan_file() and scan_file_iter() Python API functions.
  • New -v/--verbose option which shows each directory that is being scanned.
Release scan-for-secrets 0.1.1 — Scan for secrets in files you plan to share
  • Added documentation of the escaping schemes that are also scanned.
  • Removed unnecessary repr escaping scheme, which was already covered by json.
Release scan-for-secrets 0.1 — Scan for secrets in files you plan to share

I like publishing transcripts of local Claude Code sessions using my claude-code-transcripts tool but I'm often paranoid that one of my API keys or similar secrets might inadvertently be revealed in the detailed log files.

I built this new Python scanning tool to help reassure me. You can feed it secrets and have it scan for them in a specified directory:

uvx scan-for-secrets $OPENAI_API_KEY -d logs-to-publish/

If you leave off the -d it defaults to the current directory.

It doesn't just scan for the literal secrets - it also scans for common encodings of those secrets e.g. backslash or JSON escaping, as described in the README.

If you have a set of secrets you always want to protect you can list commands to echo them in a ~/.scan-for-secrets.conf.sh file. Mine looks like this:

llm keys get openai
llm keys get anthropic
llm keys get gemini
llm keys get mistral
awk -F= '/aws_secret_access_key/{print $2}' ~/.aws/credentials | xargs

I built this tool using README-driven-development: I carefully constructed the README describing exactly how the tool should work, then dumped it into Claude Code and told it to build the actual tool (using red/green TDD, naturally.)

Release research-llm-apis 2026-04-04 — Research into the HTTP APIs from various LLM providers.

I'm working on a major change to my LLM Python library and CLI tool. LLM provides an abstraction layer over hundreds of different LLMs from dozens of different vendors thanks to its plugin system, and some of those vendors have grown new features over the past year which LLM's abstraction layer can't handle, such as server-side tool execution.

To help design that new abstraction layer I had Claude Code read through the Python client libraries for Anthropic, OpenAI, Gemini and Mistral and use those to help craft curl commands to access the raw JSON for both streaming and non-streaming modes across a range of different scenarios. Both the scripts and the captured outputs now live in this new repo.

Research Can JavaScript Escape a CSP Meta Tag Inside an Iframe? — JavaScript running inside a `sandbox="allow-scripts"` iframe cannot escape or disable a `<meta http-equiv="Content-Security-Policy">` tag, even through removal, modification, or document replacement. Extensive testing across Chromium and Firefox confirmed that CSP policies defined via meta tags are enforced at parse time, and persist even when the iframe is navigated to a data: URI.

In trying to build my own version of Claude Artifacts I got curious about options for applying CSP headers to content in sandboxed iframes without using a separate domain to host the files. Turns out you can inject <meta http-equiv="Content-Security-Policy"...> tags at the top of the iframe content and they'll be obeyed even if subsequent untrusted JavaScript tries to manipulate them.

Release llm-gemini 0.30 — LLM plugin to access Google's Gemini family of models

New models gemini-3.1-flash-lite-preview, gemma-4-26b-a4b-it and gemma-4-31b-it. See my notes on Gemma 4.