Posts in Html (20 found)

Patrick Rhone

This week on the People and Blogs series we have an interview with Patrick Rhone, whose blog can be found at patrickrhone.net . Tired of RSS? Read this in your browser or sign up for the newsletter . People and Blogs is supported by the "One a Month" club members. If you enjoy P&B, consider becoming one for as little as 1 dollar a month. My name is Patrick Rhone. When I'm not trying to be the best husband and father I can be, I'm mostly known as a writer, blogger, technology consultant, speaker, mental health advocate, and general c-list internet personality. I also restore old houses as a professional hobby. I do volunteer circus rigging at a performing youth circus school as a less professional one. The very first post on my blog, Rhoneisms, is dated November 7th, 2003. Of course, I had been blogging before that, and there used to be posts dated slightly earlier. But, my blog actually began as an internally hosted one at the college I used to work for and I lost those earlier posts when I moved to a different platform and brought it public… Gosh, that seems like it was just yesterday. Not 22 years ago. Such is life. My main blog has had many different points of focus over the years. From geeky, mainly Apple, tech stuff to GTD-driven personal productivity stuff, to practical/actionable life advice stuff, to the anything I'm interested in sort of thing it is now. And, that’s exactly what a blog should be — a reflection of one's interest and attention over time. A reflection of who one is right now and where they've been. Blogs are living things that should grow at the same rate we do. I say "main" blog above because I do have a couple of other topic specific blogs (one for my home restoration work and The Cramped which is not often updated these days). I really just post anything I feel like. Links to things I find interesting. Essays of things that take me a bit longer to express. Short thought's I'm having. All sorts of things. I’m 58 years old. The internet was not even anything regular people could use until I was in my early 20s. My first "online" writing was things I posted to dial up BBS systems/communities. In the old days of the internet, it was common to have a blog just links or thoughts much like mine is today. There was no such thing as content management systems (like Moveable Type or WordPress) or services. No such thing as blogging software. Things were hand coded HTML. There were no “rules” about what a post had to look like or be. Here’s Kottke.org from 2001 . No titles. No format. Just some thoughts and a bunch of links for the day. This is the feel I’m trying to recapture. I generally do not have a specific creative environment. I believe the best inspiration can strike anywhere at anytime for the type of blogging I'm doing. That said, for my longer form essays, in general my process is that I think about something for a very long time and then suddenly, out of nowhere at often at the most inconvenient time, what I call "writing brain" kicks in and I must find something — anything — to get it written down ASAP. It appears fully formed when that happens. So, no drafts. My blog and domain registration is through Dreamhost who I've used for too long to remember (2012 maybe). It runs on WordPress. If I'm on iOS I use Drafts to post to it. On my Mac, I use MarsEdit . I very rarely use the Wordpress web interface for posting. Only if I need to jump in and edit the HTML of something complicated to format otherwise. Nope. I'm very happy with where it is now and how it exists. Like I said, a blog should grow and change at the same rate I do so, who knows, that could change tomorrow and when/if it does, I'll change it accordingly. Back of the napkin calculation: My general unlimited hosting for all my domains (I have a lot), sites, etc. is $39.95 a month. It would be too difficult to break down how much it is just to host the one blog out of that. It doesn't generate any direct revenue really and I don't do it for that reason. I suppose people who enjoy my work will buy one of my books or something but it is not for this that I do it. I blog because it is the best way for me to catalog my interests and thinking over time. If others want to monetize their work that's their choice and I have no real opinion on it. There are a few bloggers that I support with my dollars in different ways and I'm happy to do so. I remain a fan of Nicholas Bate who currently blogs at Hunter Gatherer 21C . In general, I enjoy his thoughts and insights. I also like his style of blogging. In many ways similar to mine (and I'd be remiss if I did not admit that mine is somewhat inspired by his). I'd recommend him for sure. But, there are too many people I absolutely adore and admire to list here. Some of which have already appeared in this series. Annie Muller , Rebecca Toh , Kurt Harden , my friend Jamie Thingelstad . Obviously also internet famous ones like Jason Kottke and John Gruber . The wonderful thing about the internet and the resurgence of blogging is that there is an endless amount of great blogs and bloggers out there. There is something and someone for everyone. Google your interests and find your people. Well, I'm writing this in the middle of a tumultuous time not just in my country but in my city and local community. It is the end of January in Minneapolis/Saint Paul and anyone reading this - even long after - need only google to know what is happening here. And, I can tell you anything you do see or read or hear about it is but one of hundreds or thousands of stories. In other words, my mind is a bit pre-occupied right now. But what I do want people to know about that is that despite everything our own federal government is doing to our state, it is only making our local communes stronger. We are deepening our ties with our neighbors, developing mutual aid networks to ensure care for the most vulnerable, and building peaceful resistance rapid response groups on a hyper local level. So this is what I want people to know: The worst of them is bringing out the best of us. The worst in them is bringing out the best in us. Now that you're done reading the interview, go check the blog and subscribe to the RSS feed . If you're looking for more content, go read one of the previous 132 interviews . People and Blogs is possible because kind people support it.

0 views

Superpowers 5

Superpowers 5 is out today. By far, my favorite new feature is the "Visual Brainstorming" companion tool, which grew out of my frustration with the ASCII art that Claude usually generates when you ask it anything about design or UX. I found myself asking Claude, over and over, "Hey, why don't you write that out as HTML so I can see what you're talking about." It took me far too long to remember what might be the most important mantra of agentic coding: "Why am I doing this?" And so now, if your agent has Superpowers and thinks that it has something for you to see, it'll prompt you: Some of what we'll be working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL) As an example of how this works, I fired up an instance of Claude with Superpowers 5 to clean up the rendered webpages from Youtube2Webpage . Until quite recently, Youtube2Webpage was, by far, my most popular GitHub repo. Thanks to all of you, that is no longer the case. And then after a little more back and forth in the terminal, Claude told me to go look at the browser. Visual Brainstorming has been in our dev branch for a couple months now. Since it landed, Claude Code's built in "AskUserQuestion" tool has grown support for showing ASCII-art diagrams attached to choices: ASCII art is great for communicating basic intent, but when you're working anything complicated, getting out of the terminal can make it much easier to communicate about what you're doing. As an example, I tried to get Claude to help with some brand/logo ideation for Prime Radiant . You can imagine what kind of a disaster that would have been in ASCII art. Behind the scenes, we're spinning up a web server that loads chunks of HTML written by your agent off of disk and a little bit of client-side javascript that returns clicks and feedback from the browser as you interact with it. Visual Brainstorming is mostly tested in Claude Code and Codex, but should work in most agents. One of the most important workflow improvements in 5.0 is a new 'spec review' loop. Starting with Claude Opus 4.5 or so, I've been catching my agents leaving some steps in longer spec and planning docs as "TBD" or "Fill this in later." Using specs and plans with "TBD" sections goes over just about as poorly as you'd imagine it would. The solution to something like this is to run the exact same playbook we're running everywhere else: an adversarial review loop. Now, after Superpowers finishes planning, it kicks off a subagent that reads the plannning docs for sanity and completeness. It's not a panacea and not a substitute for actually glancing at at least the "spec" doc yourself, but does seem to lead to a dramatic improvement in planning document quality. Until now, Superpowers has always offered the user the choice of Subagent Driven Development or having the human partner open up another session and run a plan one chunk at a time. That choice dates from a time when subagents were new and, well, I didn't trust them. Over the past 5 months, it's become crystal clear to me that the Subagent Driven Development workflow is dramatically more capable and effective than the old way. If your coding agent supports subagents, Superpowers will use Subagent Driven Development. If it doesn't support subagents, it will warn you that a harness that supports subagents will do a better job and then it'll do the best it can to work the full plan in a single session. In harnesses like Claude Code that are capable of choosing which model to use for a subagent, we now instruct the agent to use the cheapest model capable of doing a given task. With the detailed plans produced through the brainstorming + writing plans process, it's not uncommon to be able to use Claude Haiku for implementation. Along with that, I've tuned Subagent Driven Development a little bit to allow the subagents to better communicate if they're out of their depth and need a hand. As it becomes more realistic to build larger and larger projects with Superpowers, I've found it helpful to add some additional general software engineering guidance to the core skills. I asked Claude what it understood about these changes and this is what it had to say: One thing that Claude neglected to mention is that brainstorming is now on the lookout for projects that it considers "too big" and will interactively work with you to break them down into more manageable pieces. You'll likely see even more aggressive work on task decomposition in a future release. As of 5.0, Superpowers no longer defaults to writing its specs and plans in docs/plans, instead preferring docs/superpowers/specs and docs/superpowers/plans. Superpowers now explicitly instructs your coding agent to prefer direct instructions from you, your CLAUDE.md or your AGENTS.md to Superpowers internal instructions. If you want to customize Superpowers behavior, it should now be as simple as a line in the right document. OpenAI Codex has recently added support for subagents. It's been a little bit of a moving target, but generally I've been finding that Codex rigorously follows all instructions from Superpowers. And that includes when subagents get ahold of the skill and decide to start brainstorming and delegating all their work to subagents, occasionally recursively. Superpowers 5 adds a mitigation against this behavior. The slash commands that have been in Superpowers since the beginning date from a time when Claude Code didn't have native support for skills. The original prompts that became the brainstorm, writing-plans, and executing-plans skills lived in slash commands. As skills have evolved, Claude Code treats skills as slash commands and Claude is increasingly confused by the 'old' slash commands. As of 5.0, the three slash commands we ship now announce that they've been deprecated. They'll go away in a future release. In general, you should be able to just describe your intent to your coding agent and Superpowers + the native skills system should start using the right skills. (My default smoke test for Superpowers is to open up a coding agent and type "Let's make a react todo list." If the agent starts coding, I failed. If it kicks off the brainstorming skill, then the right thing is happening.) Superpowers 5 is out now. If you're using a harness with a plugin system like Claude Code or Cursor, it should auto-update sometime over the next day or two. If you're using Superpowers in another tool with a direct installation, you may need to git pull or instruct your agent to do a fresh install.

0 views
Xe Iaso 6 days ago

Vibe Coding Trip Report: Making a sponsor panel

I'm on medical leave recovering from surgery . Before I went under, I wanted to ship one thing I'd been failing to build for months: a sponsor panel at sponsors.xeiaso.net . Previous attempts kept dying in the GraphQL swamp. This time I vibe coded it — pointed agent teams at the problem with prepared skills and let them generate the gnarly code I couldn't write myself. And it works. Go and GraphQL are oil and water. I've held this opinion for years and nothing has changed it. The library ecosystem is a mess: shurcooL/graphql requires abusive struct tags for its reflection-based query generation, and the code generation tools produce mountains of boilerplate. All of it feels like fighting the language into doing something it actively resists. GitHub removing the GraphQL explorer made this even worse. You used to be able to poke around the schema interactively and figure out what queries you needed. Now you're reading docs and guessing. Fun. I'd tried building this panel before, and each attempt died in that swamp. I'd get partway through wrestling the GitHub Sponsors API into Go structs, lose momentum, and shelve it. At roughly the same point each time: when the query I needed turned out to be four levels of nested connections deep and the struct tags looked like someone fell asleep on their keyboard. Vibe coding was a hail mary. I figured if it didn't work, I was no worse off. If it did, I'd ship something before disappearing into a hospital for a week. Vibe coding is not "type a prompt and pray." Output quality depends on the context you feed the model. Templ — the Go HTML templating library I use — barely exists in LLM training data. Ask Claude Code to write Templ components cold and it'll hallucinate syntax that looks plausible but doesn't compile. Ask me how I know. Wait, so how do you fix that? I wrote four agent skills to load into the context window: With these loaded, the model copies patterns from authoritative references instead of inventing syntax from vibes. Most of the generated Templ code compiled on the first try, which is more than I can say for my manual attempts. Think of it like giving someone a cookbook instead of asking them to invent recipes from first principles. The ingredients are the same, but the results are dramatically more consistent. I pointed an agent team at a spec I'd written with Mimi . The spec covered the basics: OAuth login via GitHub, query the Sponsors API, render a panel showing who sponsors me and at what tier, store sponsor logos in Tigris . I'm not going to pretend I wrote the spec alone. I talked through the requirements with Mimi and iterated on it until it was clear enough for an agent team to execute. The full spec is available as a gist if you want to see what "clear enough for agents" looks like in practice. One agent team split the spec into tasks and started building. A second reviewed output and flagged issues. Meanwhile, I provisioned OAuth credentials in the GitHub developer settings, created the Neon Postgres database, and set up the Tigris bucket for sponsor logos. Agents would hit a point where they needed a credential, I'd paste it in, and they'd continue — ops work and code generation happening in parallel. The GraphQL code the agents wrote is ugly . Raw query strings with manual JSON parsing that would make a linting tool weep. But it works. The shurcooL approach uses Go idioms, sure, but it requires so much gymnastics to handle nested connections that the cognitive load is worse. Agent-generated code is direct: send this query string, parse this JSON, done. I'd be embarrassed to show it at a code review. I'd also be embarrassed to admit how many times I failed to ship the "clean" version. This code exists because the "proper" way kept killing the project. I'll take ugly-and-shipped over clean-and-imaginary. The full stack: Org sponsorships are still broken. The schema for organization sponsors differs enough from individual sponsors that it needs its own query path and auth flow. I know what the fix looks like, but it requires reaching out to other devs who've cracked GitHub's org-level sponsor queries. The code isn't my usual style either — JSON parsing that makes me wince, variable names that are functional but uninspired, missing error context in a few places. I'll rewrite chunks of this after I've recovered. The panel exists now, though. It renders real data. People can OAuth in and see their sponsorship status. Before this attempt, it was vaporware. I've been telling people "just ship it" for years. Took vibe coding to make me actually do it myself. I wouldn't vibe code security-critical systems or anything I need to audit line-by-line. But this project had stopped me cold on every attempt, and vibe coding got it across the line in a weekend. Skills made the difference here. Loading those four documents into the context window turned Claude Code from "plausible but broken Templ" into "working code on the first compile." I suspect that gap will only matter more as people try to use AI with libraries that aren't well-represented in training data. This sponsor panel probably won't look anything like it does today in six months. I'll rewrite the GraphQL layer once I find a pattern that doesn't make me cringe. Org sponsorships still need work. HTMX might get replaced. But it exists, and before my surgery, shipping mattered more than polish. The sponsor panel is at sponsors.xeiaso.net . The skills are in my site's repo under . templ-syntax : Templ's actual syntax, with enough detail that the model can look up expressions, conditionals, and loops instead of guessing. templ-components : Reusable component patterns — props, children, composition. Obvious if you've used Templ, impossible to infer from sparse training data. templ-htmx : The gotchas when combining Templ with HTMX. Attribute rendering and event handling trip up humans and models alike. templ-http : Wiring Templ into handlers properly — routes, data passing, request lifecycle. Go for the backend, because that's what I know and what my site runs on Templ for HTML rendering, because I'm tired of 's limitations HTMX for interactivity, because I refuse to write a React app for something this simple PostgreSQL via Neon for persistence GitHub OAuth for authentication GitHub Sponsors GraphQL API for the actual sponsor data Tigris for sponsor logo storage — plugged it in and it Just Works™

0 views
Frederik Braun 1 weeks ago

Composing Sanitizer configurations

The HTML Sanitizer API allows multiple ways to customize the default allow list and this blog post aims to describe a few variations and tricks we came up with while writing the specification. Examples in this post will use configuration dictionaries. These dictionaries might be used …

0 views
Frederik Braun 1 weeks ago

Perfect types with `setHTML()`

TLDR: Use in your CSP and nothing besides works, essentially removing all DOM-XSS risks. I was guest at the ShopTalkShow Podcast to talk about and the HTML Sanitizer API. Feel free to listen to the whole episode, if you want to …

0 views
Lea Verou 1 weeks ago

External import maps, today!

A few weeks ago, I posted Web dependencies are broken. Can we fix them? . Today’s post is a little less gloomy: Turns out that the major limitation that would allow centralized set-it-and-forget-it import map management can be lifted today, with excellent browser support! The core idea is that you can use DOM methods to inject an import map dynamically , by literally creating an element in a classic (blocking) script and appending it after the injector script. 💡 This is a gamechanger. It makes external import maps nice-to-have sugar instead of the only way to have centralized import map management decoupled from HTML generation. All we need to do is build a little injector script, no need for tightly coupled workflows that take over everything. Once you have that, it takes a single line of HTML to include it anywhere. If you’re already using a templating system, great! You could add to your template for every page. But you don’t need a templating system: even if you’re rawdogging HTML (e.g. for a simple SPA), it’s no big deal to just include a in there manually. This is not even new: when the injector is a classic (non-module) script placed before any modules are fetched, it works in every import map implementation , all the way back to Chrome 89, Safari 16.4+, and Firefox 108+ ! Turns out, JSPM made the same discovery: JSPM v4 uses the same technique . It is unclear why it took all of us so long to discover it but I’m glad we got there. First, while there is some progress around making import maps more resilient, your best bet for maximum compatibility is for the injector script to be a good ol’ blocking that comes before everything else. This means no , no , no — you want to get it in before any modules start loading or many browsers will ignore it. Then, you literally use DOM methods to create a and append it after the script that is injecting the import map (which you can get via ). This is a minimal example: Remember, this literally injects inline import maps in your page. This means that any relative URLs will be interpreted relative to the current page ! If you’re building an SPA or your URLs are all absolute or root-relative, that’s no biggie. But if these are relative URLs, they will not work as expected across pages. You need to compute the absolute URL for each mapped URL and use that instead. This sounds complicated, but it only adds about 5 more lines of code: Note that is in module scripts, since the same module can be loaded from different places and different scripts. Once it becomes possible to inject import maps from a module script, you could use to get the URL of the current module. Until then, you can use a bit of error handling to catch mistakes: This is the minimum, since the script literally breaks if is . You could get more elaborate and warn about / attributes, or if scripts are present before the current script. These are left as an exercise for the reader. While this alleviates the immediate need for external import maps , the DX and footguns make it a bit gnarly, so having first-class external import map support would still be a welcome improvement. But even if we could do today, the unfortunate coupling with HTML is still at the receiving end of all this, and creates certain limitations, such as specifiers not working in worker scripts . My position remains that HTML being the only way to include import maps is a hack . I’m not saying this pejoratively. Hacks are often okay — even necessary! — in the short term. This particular hack allowed us to get import maps out the door and shipped quickly, without getting bogged down into architecture astronaut style discussions that can be non-terminating. But it also created architectural debt . These types of issues can always be patched ad hoc, but that increases complexity, both for implementers and web developers. Ultimately, we need deeper integration of specifiers and import maps across the platform . (with or without an attribute) should become a shortcut , not the only way mappings can be specified. In my earlier post, I outlined a few ideas that could help get us closer to that goal and make import maps ubiquitous and mindless . Since they were well received, I opened issues for them: I’m linking to issues in standards repos in the interest of transparency. Please don’t spam them, even with supportive comments (that’s what reactions are for). Also keep in mind that the vast majority of import map improvements are meant for tooling authors and infrastructure providers — nobody expects regular web developers to author import maps by hand. The hope is also that better platform-wide integration can pave the way for satisfying the (many!) requests to expand specifiers beyond JS imports . Currently, the platform has no good story for importing non-JS resources from a package, such as styles, images, icons, etc. But even without any further improvement, simply the fact that injector scripts are possible opens up so many possibilities! The moment I found out about this I started working on making the tool I wished had existed to facilitate end-to-end dependency management without a build process (piggybacking on the excellent JSPM Generator for the heavy lifting), which I will announce in a separate post very soon [1] . Stay tuned! But if you’re particularly curious and driven, you can find it even before then, both the repo and npm package are already public 😉🤫 ↩︎ Linking to import maps via an HTTP header ( ?) URLs to bridge the gap between specifiers and URLs Since import maps as an import attribute proved out to be tricky, I also filed another proposal for a synchronous import map API . But if you’re particularly curious and driven, you can find it even before then, both the repo and npm package are already public 😉🤫 ↩︎

0 views
devansh 1 weeks ago

Hacking Better-Hub

Better-Hub ( better-hub.com ) is an alternative GitHub frontend — a richer, more opinionated UI layer built on Next.js that sits on top of the GitHub API. It lets developers browse repositories, view issues, pull requests, code blobs, and repository prompts, while authenticating via GitHub OAuth. Because Better-Hub mirrors GitHub content inside its own origin, any unsanitized rendering of user-controlled data becomes significantly more dangerous than it would be on a static page — it has access to session tokens, OAuth credentials, and the authenticated GitHub API. That attack surface is exactly what I set out to explore. Description The repository README is fetched from GitHub, piped through with and enabled — with zero sanitization — then stored in the state and rendered via in . Because the README is entirely attacker-controlled, any repository owner can embed arbitrary JavaScript that executes in every viewer's browser on better-hub.com. Steps to Reproduce Session hijacking via cookie theft, credential exfiltration, and full client-side code execution in the context of better-hub.com. Chains powerfully with the GitHub OAuth token leak (see vuln #10). Description Issue descriptions are rendered with the same vulnerable pipeline: with raw HTML allowed and no sanitization. The resulting is inserted directly via inside the thread entry component, meaning a malicious issue body executes arbitrary script for every person who views it on Better-Hub. Steps to Reproduce Arbitrary JavaScript execution for anyone viewing the issue through Better-Hub. Can be used for session hijacking, phishing overlays, or CSRF-bypass attacks. Description Pull request bodies are fetched from GitHub and processed through with / and no sanitization pass, then rendered unsafely. An attacker opening a PR with an HTML payload in the body causes XSS to fire for every viewer of that PR on Better-Hub. Steps to Reproduce Stored XSS affecting all viewers of the PR. Particularly impactful in collaborative projects where multiple team members review PRs. Description The same unsanitized pipeline applies to PR comments. Any GitHub user who can comment on a PR can inject a stored XSS payload that fires for every Better-Hub viewer of that conversation thread. Steps to Reproduce A single malicious commenter can compromise every reviewer's session on the platform. Description The endpoint proxies GitHub repository content and determines the from the file extension in the query parameter. For files it sets and serves the content inline (no ). An attacker can upload a JavaScript-bearing SVG to any GitHub repo and share a link to the proxy endpoint — the victim's browser executes the script within 's origin. Steps to Reproduce Reflected XSS with a shareable, social-engineered URL. No interaction with a real repository page is needed — just clicking a link is sufficient. Easily chained with the OAuth token leak for account takeover. Description When viewing code files larger than 200 KB, the application hits a fallback render path in that outputs raw file content via without any escaping. An attacker can host a file exceeding the 200 KB threshold containing an XSS payload — anyone browsing that file on Better-Hub gets the payload executed. Steps to Reproduce Any repository owner can silently weaponize a large file. Because code review is often done on Better-Hub, this creates a highly plausible attack vector against developers reviewing contributions. Description The function reads file content from a shared Redis cache . Cache entries are keyed by repository path alone — not by requesting user. The field is marked as shareable, so once any authorized user views a private file through the handler or the blob page, its contents are written to Redis under a path-only key. Any subsequent request for the same path — from any user, authenticated or not — is served directly from cache, completely bypassing GitHub's permission checks. Steps to Reproduce Complete confidentiality breach of private repositories. Any file that has ever been viewed by an authorized user is permanently exposed to unauthenticated requests. This includes source code, secrets in config files, private keys, and any other sensitive repository content. Description A similar cache-keying problem affects the issue page. When an authorized user views a private repo issue on Better-Hub, the issue's full content is cached and later embedded in Open Graph meta properties of the page HTML. A user who lacks repository access — and sees the "Unable to load repository" error — can still read the issue content by inspecting the page source, where it leaks in the meta tags served from cache. Steps to Reproduce Private issue contents — potentially including bug reports, credentials in descriptions, or internal discussion — are accessible to any unauthenticated party who knows or guesses the URL. Description Better-Hub exposes a Prompts feature tied to repositories. For private repositories, the prompt data is included in the server-rendered page source even when the requestor does not have repository access. The error UI correctly shows "Unable to load repository," but the prompt content is already serialized into the HTML delivered to the browser. Steps to Reproduce Private AI prompts — which may contain internal instructions, proprietary workflows, or system prompt secrets — leak to unauthenticated users. Description returns a session object that includes . This session object is passed as props directly to client components ( , , etc.). Next.js serializes component props and embeds them in the page HTML for hydration, meaning the raw GitHub access token is present in the page source and accessible to any JavaScript running on the page — including scripts injected via any of the XSS vulnerabilities above. The fix is straightforward: strip from the session object before passing it as props to client components. Token usage should remain server-side only. When chained with any XSS in this report, an attacker can exfiltrate the victim's GitHub OAuth token and make arbitrary GitHub API calls on their behalf — reading private repos, writing code, managing organizations, and more. This elevates every XSS in this report from session hijacking to full GitHub account takeover . Description The home page redirects authenticated users to the destination specified in the query parameter with no validation or allow-listing. An attacker can craft a login link that silently redirects the victim to an attacker-controlled domain immediately after they authenticate. Steps to Reproduce Phishing attacks exploiting the trusted better-hub.com domain. Can be combined with OAuth token flows for session fixation attacks, or used to redirect users to convincing fake login pages post-authentication. All issues were reported directly to Better-Hub team. The team was responsive and attempted rapid remediation. What is Better-Hub? The Vulnerabilities 01. Unsanitized README → XSS 02. Issue Description → XSS 03. Stored XSS in PR Bodies 04. Stored XSS in PR Comments 05. Reflected XSS via SVG Image Proxy 06. Large-File XSS (>200 KB) 07. Cache Deception — Private File Access 08. Authz Bypass via Issue Cache 09. Private Repo Prompt Leak 10. GitHub OAuth Token Leaked to Client 11. Open Redirect via Query Parameter Disclosure Timeline Create a GitHub repository with the following content in : View the repository at and observe the XSS popup. Create a GitHub issue with the following in the body: Navigate to the issue via to trigger the payload. Open a pull request whose body contains: View the PR through Better-Hub to observe the XSS popup. Post a PR comment containing: View the comment thread via Better-Hub to trigger the XSS. Create an SVG file in a public GitHub repo with content: Direct the victim to: Create a file named containing the payload, padded to exceed 200 KB: Browse to the file on Better-Hub at . The XSS fires immediately. Create a private repository and add a file called . As the repository owner, navigate to the following URL to populate the cache: Open the same URL in an incognito window or as a completely different user. The private file content is served — no authorization required. Create a private repo and create an issue with a sensitive body. Open the issue as an authorized user: Open the same URL in a different session (no repo access). While the access-error UI is shown, view the page source — issue details appear in the tags. Create a private repository and create a prompt in it. Open the prompt URL as an unauthorized user: View the page source — prompt details are present in the HTML despite the access-denied UI. Log in to Better-Hub with GitHub credentials. Navigate to: You are immediately redirected to .

0 views
JSLegendDev 1 weeks ago

If You Like PICO-8, You'll Love KAPLAY (Probably)

I’ve been checking out PICO-8 recently. For those unaware, It’s a nicely constrained environment for making small games in Lua. It provides a built-in editor allowing you to write code, make sprites, make tile maps and make sounds. This makes it ideal to prototype game ideas or make small games. You know what tool is also great for prototyping game ideas or making small games? Well… KAPLAY ! It’s a simple free and open source library for making games in JavaScript. I suspect there might be a sizeable overlap between people who like PICO-8 and those who would end up liking or even loving KAPLAY as well if they gave it a try. During my PICO-8 learning journey, I came across this nice tutorial teaching you how to make a coin collecting game in 10 minutes. In this article, I’d like to teach you how to build roughly the same game in KAPLAY. This will better demonstrate in what ways this game library makes game development faster much like PICO-8. Feel free to follow along if you wish to! KAPLAY lacks all of the tools included in PICO-8. There is no all-in-one package you can use to write your code, make your sprites, build your maps or even make sounds. You might be wondering, then, how KAPLAY is in any way similar to PICO-8 if it lacks all of this? My answer : KAPLAY makes up for it by making the coding part really easy by offering you a lot logic built-in. For example, it handles collisions, physics, scene management, animations etc… for you. You’ll see some of this in action when we arrive at the part where we write the game’s code. Now, how do we use KAPLAY? Here’s the simplest way I’ve found. You install VSCode (a popular code editor) along with the Live Server extension (can be found in the extensions marketplace within the editor). You then create a folder that you open within VSCode. Once the folder is opened, we only need to create two files. One called index.html and the other main.js. Your index.html file should contain the following : Since KAPLAY works on the web, it lives within a web page. index.html is that page. Then, we link our JavaScript file to it. We set the type to “module” so we can use import statements in our JS. We then add the following : Voilà! We can now use the KAPLAY library. Since we installed the Live Server extension, you should now have access to a “Go Live” button at the bottom of the editor. To actually run the game, all you have to do is click it. This will open the web page in your default browser. KAPLAY by default creates a canvas with a checkered pattern. One thing pretty cool with this setup, is that every time you change something in your code and hit save (Ctrl+S or Cmd+S on a Mac), the web page reloads and you can see your latest changes instantly. I’ve created the following spritesheet to be used in our game. Note that since the image is transparent, the cloud to the right is not really visible. You can download the image above to follow along. The next step is to place the image in the same folder as our HTML page and JavaScript file. We’re now ready to make our game. Here we set the width and the height of our canvas. The letterbox option is used to make sure the canvas scales according to the browser window but without losing its aspect ratio. We can load our spritesheet by using the loadSprite KAPLAY function. The first param is the name you want to use to refer to it elsewhere in your code. The second param is the path to get that asset. Finally, the third param is used to tell KAPLAY how to slice your image into individual frames. Considering that in our spritesheet we have three sprites placed in a row, the sliceX property should be set to 3. Since we have only one sprite per column (because we only have one column) sliceY should be set to 1. To make the coins fall from the top, we’ll use KAPLAY’s physics system. You can set the gravity by calling KAPLAY’s setGravity function. KAPLAY’s add function is used to create a game object by providing an array of components. These components are offered by KAPLAY and come with prebuilt functionality. The rect() component sets the graphics of the game object to be a rectangle with a width and height of 1000. On the other hand, the color component sets its color. You should have the following result at this stage. Creating The Basket The basket is a also a game object with several different components. Here is what each does : Sets the sprite used by the game object. The first param is for providing the name of the sprite we want to use. Since we’re using a spritesheet which contains three different sprites in the same image, we need to specify the frame to use. The basket sprite corresponds to frame 0. anchor() By default, game objects are positioned based on their top-left corner. However, I prefer having it centered. The anchor component is for this purpose. This component is used to set the position of the game object on the canvas. Here we also use center() which is a KAPLAY provided function that provides the coordinates of the center of the canvas. This component is used to set the hitbox of a game object. This will allow KAPLAY’s physics system to handle collisions for us. There is a debug mode you can access by pressing the f1 (fn+f1 on Mac) key which will make hitboxes visible. Example when debug mode is on. As for setting the shape of the hitbox, you can use the Rect class which takes 3 params. The first expects a vec2 (a data structure offered by KAPLAY used to set pair of values) describing where to place the hitbox relative to the game object. If set to 0, the hitbox will have the same position as the game object. The two params left are used to set its width and height. Finally, the body component is used to make the game object susceptible to physics. If added alone, the game object will be affected by gravity. However, to prevent this, we can set the isStatic property to true. This is very useful, for example, in a platformer where platforms need to be static so they don’t fall off. Here we can use the move method available on all game objects to make the basket move in the desired direction. The loop function spawns a coin every second. We use the randi function to set a random X position between 10 and 950. The offscreen component is used to destroy the game object once it’s out of view. Finally a simple string “coin” is added alongside the array of components to tag the game object being created. This will allow us to determine which coin collided with the basket so we can destroy it and increase the score. Text can be displayed by creating a game object with the text component. To know when a coin collides with the basket, we can use its onCollide method (available by default). The first param of that method is the tag of the game object you want to check collisions with. Since we have multiple coins using the “coin” tag, the specific coin currently colliding with the basket will be passed as a param to the collision handler. Now we can destroy the coin, increase the score and display the new score. As mentioned earlier, KAPLAY does not have a map making tool. However, it does offer the ability to create maps using arrays of strings. For anything more complex, you should check out Tiled which is also open source and made for that purpose. Where we place the # character in the string array determines where clouds will be placed in the game. Publishing a KAPLAY game is very simple. You compress your folder into a .zip archive and you upload it to itch.io or any other site you wish to. The game will be playable in the browser without players needing to download it. Now, what if you’d like to make it downloadable as well? A very simple tool you can use is GemShell. It allows you to make executables for Windows/Mac/Linux in what amounts to a click. You can use the lite version for free. If you plan on upgrading, you can use my link to get 15% off your purchase. To be transparent, this is an affliate link. If you end purchasing the tool using my link, I’ll get a cut of that sale. I just scratched the surface with KAPLAY today. I hope it gave you a good idea of what it’s like to make games with it. If you’re interested in more technical articles like this one, I recommend subscribing to not miss out on future publications. Subscribe now In the meantime, you can check out the following :

0 views
Hugo 2 weeks ago

Dogfooding: Why I Migrated My Own Blog to Writizzy

In 2022, I created an open-source static blog generator: Bloggrify . It’s conceptually similar to Hugo —it generates a static site (just a bunch of HTML files) that you can host for free on Cloudflare, GitHub Pages, or Bunny.net . Before that, I had tried everything: WordPress, Joomla, Medium. I wanted to regain flexibility and customize my blog exactly how I wanted. But let’s be honest: I’m a developer, and I mainly wanted a new technical playground. Fast forward to 2026, and I have to admit: using a static blog has become a major friction point for my writing. So, I decided to migrate again, this time to a managed platform: Writizzy , another product I’m building. This move is a great opportunity to talk about several things: Bloggrify started as a love letter to the Nuxt ecosystem, specifically . Back when I migrated from WordPress, my criteria were simple: In 2022, it wasn't a "product" yet—just my personal blog code made public. It only became a full-fledged open-source project in 2024, with a dedicated site and a proper README to encourage contributions. I wanted the product to be "opinionated." Nuxt-content does 90% of the heavy lifting, but it’s a generic tool. For a real blog, you still need to build the RSS feed, sitemap, robots.txt, comments, table of contents, share buttons, newsletter integration, analytics, and SEO. That’s what Bloggrify is: a "starter pack" that comes with everything pre-configured. Think of it as Docus , but for blogs instead of documentation. I’m a numbers person. When I launch a project, I want to see usage. It might sound trivial, but considering the effort it takes to manage NPM releases (which is honestly a nightmare), handle versioning, and maintain themes, you expect a minimum return on investment. Bloggrify reached 164 stars on GitHub and sits somewhere in the middle of the pack on Jamstack.org . That’s... okay, I guess. But in reality, I have almost zero feedback on its actual usage. A few rare GitHub issues, one contributor who was active for a few weeks before vanishing, and then silence. I only know of one blog that used it before switching back to Hugo. The experience has been bittersweet. Building in the dark is demotivating. However, it did lead me to launch two other side-products: I launched Broadcast and Pulse in 2024 and 2025. They’re living a quiet life, but they aren't "exploding." My target audience is static bloggers—mostly developers. And as we know, developers are the hardest group to convince to pay for a service! Still, I’m satisfied. These products taught me how to build a SaaS, handle subscriptions, and find my ideal tech stack. My own newsletters were sent via Broadcast (reaching about 150 subscribers), and I used Pulse to track which articles were actually being read. The reality? These two tools generate about €100 in Monthly Recurring Revenue (MRR) . Not enough to retire on, but a great learning experience. And that brings us to Writizzy. With Bloggrify, I realized my writing workflow had become painful. Between maintaining the framework, jumping between spell-checkers, writing in Markdown, spinning up a local server to check for broken links, and waiting for build/deployment times... I was losing hours. For my last article, someone pointed out a few typos. It took me 20 minutes between editing the file and seeing the fix live. Add to that the friction of managing images in an IDE and the recent Nuxt 4 / Nuxt-content updates which, while I love them, have made the dev experience slightly slower for simple blogging. To be honest, I wasn't aware of that. I put up with these inconveniences and was still very happy to have “flexibility” in what I could do with my blog. I wasn't fully aware of this "friction" until I built Writizzy . Writizzy is the synthesis of my blogging experience. It’s a mix of Substack, Ghost, and Medium, but built as a European alternative with four core pillars: I moved my English blog to Writizzy first (this one), with no intention of moving the french one. But I soon noticed I was writing much faster on the English site. The workflow was just... better. Copy-pasting images directly into the editor, instant previews, no server to launch. It was a joy. I hesitated for a long time before migrating eventuallycoding.com . I knew that by doing so, I was taking the risk of killing Bloggrify. If even I don't use it anymore, the project enters a danger zone. When you don’t use your own product daily—when you’re no longer obsessed with the problem it solves—it’s almost impossible to stay attached to it. This is a symptom I see in so many "disposable" projects across the internet: built by people who flutter from one idea to the next without any real skin in the game. So yes, moving away from Bloggrify is a risk. But I’ve come to terms with it. Today, I have almost zero evidence that Bloggrify is being used. Meanwhile, Writizzy already has 314 blogs and 11 paying users (€135 MRR) in just four months. Why stubbornly cling to Bloggrify? Ultimately, I believe I’m solving the same problem with Writizzy, but in a much better way. I receive feedback emails and feature requests every single week. I get constant positive reinforcement from people actually using it. The product isn’t perfect, but it improves every day. It improves because real users are pushing me to refine the site, fix what’s broken, and add the features that absolutely need to be there. And it also improves because I use it constantly. This is the massive benefit of dogfooding . Every day, I am confronted with my own software, so I know exactly what needs to change. So yes, Bloggrify is moving to maintenance mode. I’m taking this opportunity to turn all templates into Open Source. Two of them were "premium," but it wouldn't make sense to keep them that way today. I tell myself I’ll still evolve it from time to time, but honestly, I wonder if I’m just lying to myself. As for Hakanai.io , I’m definitely continuing. The problem it solves still fascinates me. I get great feedback, especially on Broadcast. Pulse , however, suffers from being misunderstood. It’s a "blog analytics" product, and people don't really grasp what that entails—SEO advice, outlier detection, evergreen content tracking. I’m not great at marketing, so it mostly flies under the radar except for the readers of this blog who took the time to test it. But I’m motivated to keep them alive. As for Writizzy , there is no doubt. The product is incredibly exciting to build. The stakes are high: building a platform for expression that exists outside the US-centric giants. The traction is there, and the numbers follow (+45% MoM user growth). Welcome to this blog, now officially on Writizzy. As a reader, you can already test several things: The Discover feed to read other articles from Writizzy bloggers. We’ve handpicked a few to start with, and this feed will become even more customizable in the future. Welcome home. Dogfooding: Why you absolutely must use your own products. The harsh reality of Open Source: Why it’s harder than it looks. Product Satisfaction: The joy of building something people actually use. The future of my projects: Bloggrify, Writizzy, and Hakanai.io . A simple templating language (Markdown). Extensibility (RSS feeds, sitemaps, etc.). Low carbon footprint (static sites are incredibly efficient). broadcast.hakanai.io : A newsletter system for static blogs based on RSS feeds. pulse.hakanai.io : A specialized analytics tool for bloggers (not just generic web traffic). Sustainability : Focusing on reversibility and interoperability. Discoverability. Economic accessibility : Implementing Purchasing Power Parity (PPP). Transparency. The comments section . The newsletter subscription (if you haven’t already).

0 views
Max Woolf 2 weeks ago

An AI agent coding skeptic tries AI agent coding, in excessive detail

You’ve likely seen many blog posts about AI agent coding/ vibecoding where the author talks about all the wonderful things agents can now do supported by vague anecdata, how agents will lead to the atrophy of programming skills, how agents impugn the sovereignty of the human soul, etc etc. This is NOT one of those posts. You’ve been warned. Last May, I wrote a blog post titled As an Experienced LLM User, I Actually Don’t Use Generative LLMs Often as a contrasting response to the hype around the rising popularity of agentic coding. In that post, I noted that while LLMs are most definitely not useless and they can answer simple coding questions faster than it would take for me to write it myself with sufficient accuracy, agents are a tougher sell: they are unpredictable, expensive, and the hype around it was wildly disproportionate given the results I had seen in personal usage. However, I concluded that I was open to agents if LLMs improved enough such that all my concerns were addressed and agents were more dependable. In the months since, I continued my real-life work as a Data Scientist while keeping up-to-date on the latest LLMs popping up on OpenRouter . In August, Google announced the release of their Nano Banana generative image AI with a corresponding API that’s difficult to use, so I open-sourced the gemimg Python package that serves as an API wrapper. It’s not a thrilling project: there’s little room or need for creative implementation and my satisfaction with it was the net present value with what it enabled rather than writing the tool itself. Therefore as an experiment, I plopped the feature-complete code into various up-and-coming LLMs on OpenRouter and prompted the models to identify and fix any issues with the Python code: if it failed, it’s a good test for the current capabilities of LLMs, if it succeeded, then it’s a software quality increase for potential users of the package and I have no moral objection to it. The LLMs actually were helpful: in addition to adding good function docstrings and type hints, it identified more Pythonic implementations of various code blocks. Around this time, my coworkers were pushing GitHub Copilot within Visual Studio Code as a coding aid, particularly around then-new Claude Sonnet 4.5 . For my data science work, Sonnet 4.5 in Copilot was not helpful and tended to create overly verbose Jupyter Notebooks so I was not impressed. However, in November, Google then released Nano Banana Pro which necessitated an immediate update to for compatibility with the model. After experimenting with Nano Banana Pro, I discovered that the model can create images with arbitrary grids (e.g. 2x2, 3x2) as an extremely practical workflow, so I quickly wrote a spec to implement support and also slice each subimage out of it to save individually. I knew this workflow is relatively simple-but-tedious to implement using Pillow shenanigans, so I felt safe enough to ask Copilot to , and it did just that although with some errors in areas not mentioned in the spec (e.g. mixing row/column order) but they were easily fixed with more specific prompting. Even accounting for handling errors, that’s enough of a material productivity gain to be more optimistic of agent capabilities, but not nearly enough to become an AI hypester. In November, just a few days before Thanksgiving, Anthropic released Claude Opus 4.5 and naturally my coworkers were curious if it was a significant improvement over Sonnet 4.5. It was very suspicious that Anthropic released Opus 4.5 right before a major holiday since companies typically do that in order to bury underwhelming announcements as your prospective users will be too busy gathering with family and friends to notice. Fortunately, I had no friends and no family in San Francisco so I had plenty of bandwidth to test the new Opus. One aspect of agents I hadn’t researched but knew was necessary to getting good results from agents was the concept of the AGENTS.md file: a file which can control specific behaviors of the agents such as code formatting. If the file is present in the project root, the agent will automatically read the file and in theory obey all the rules within. This is analogous to system prompts for normal LLM calls and if you’ve been following my writing, I have an unhealthy addiction to highly nuanced system prompts with additional shenanigans such as ALL CAPS for increased adherence to more important rules (yes, that’s still effective). I could not find a good starting point for a Python-oriented I liked, so I asked Opus 4.5 to make one: I then added a few more personal preferences and suggested tools from my previous failures working with agents in Python: use and instead of the base Python installation, use instead of for data manipulation, only store secrets/API keys/passwords in while ensuring is in , etc. Most of these constraints don’t tell the agent what to do, but how to do it. In general, adding a rule to my whenever I encounter a fundamental behavior I don’t like has been very effective. For example, agents love using unnecessary emoji which I hate, so I added a rule: Agents also tend to leave a lot of redundant code comments, so I added another rule to prevent that: My up-to-date file for Python is available here , and throughout my time working with Opus, it adheres to every rule despite the file’s length, and in the instances where I accidentally query an agent without having an , it’s very evident. It would not surprise me if the file is the main differentiator between those getting good and bad results with agents, although success is often mixed . As a side note if you are using Claude Code , the file must be named instead because Anthropic is weird; this blog post will just use for consistency. With my file set up, I did more research into proper methods of prompting agents to see if I was missing something that led to the poor performance from working with Sonnet 4.5. From the Claude Code quickstart . Anthropic’s prompt suggestions are simple, but you can’t give an LLM an open-ended question like that and expect the results you want! You, the user, are likely subconsciously picky, and there are always functional requirements that the agent won’t magically apply because it cannot read minds and behaves as a literal genie . My approach to prompting is to write the potentially-very-large individual prompt in its own Markdown file (which can be tracked in ), then tag the agent with that prompt and tell it to implement that Markdown file. Once the work is completed and manually reviewed, I manually commit the work to , with the message referencing the specific prompt file so I have good internal tracking. I completely ignored Anthropic’s advice and wrote a more elaborate test prompt based on a use case I’m familiar with and therefore can audit the agent’s code quality. In 2021, I wrote a script to scrape YouTube video metadata from videos on a given channel using YouTube’s Data API , but the API is poorly and counterintuitively documented and my Python scripts aren’t great. I subscribe to the SiIvagunner YouTube account which, as a part of the channel’s gimmick ( musical swaps with different melodies than the ones expected), posts hundreds of videos per month with nondescript thumbnails and titles, making it nonobvious which videos are the best other than the view counts. The video metadata could be used to surface good videos I missed, so I had a fun idea to test Opus 4.5: The resulting script is available here , and it worked first try to scrape up to 20,000 videos (the max limit). The resulting Python script has very Pythonic code quality following the copious rules provided by the , and it’s more robust than my old script from 2021. It is most definitely not the type of output I encountered with Sonnet 4.5. There was a minor issue however: the logging is implemented naively such that the API key is leaked in the console. I added a rule to but really this is the YouTube API’s fault for encouraging API keys as parameters in a GET request . I asked a more data-science-oriented followup prompt to test Opus 4.5’s skill at data-sciencing: The resulting Jupyter Notebook is…indeed thorough. That’s on me for specifying “for all columns”, although it was able to infer the need for temporal analysis (e.g. total monthly video uploads over time) despite not explicitly being mentioned in the prompt. The monthly analysis gave me an idea: could Opus 4.5 design a small webapp to view the top videos by month? That gives me the opportunity to try another test of how well Opus 4.5 works with less popular frameworks than React or other JavaScript component frameworks that LLMs push by default. Here, I’ll try FastAPI , Pico CSS for the front end (because we don’t need a JavaScript framework for this), and HTMX for lightweight client/server interactivity: The FastAPI webapp Python code is good with logical integration of HTMX routes and partials, but Opus 4.5 had fun with the “YouTube-themed” aspect of the prompt: the video thumbnail simulates a YouTube thumbnail with video duration that loads an embedded video player when clicked! The full code is open-source in this GitHub repository . All of these tests performed far better than what I expected given my prior poor experiences with agents. Did I gaslight myself by being an agent skeptic? How did a LLM sent to die finally solve my agent problems? Despite the holiday, X and Hacker News were abuzz with similar stories about the massive difference between Sonnet 4.5 and Opus 4.5, so something did change. Obviously an API scraper and data viewer alone do not justify an OPUS 4.5 CHANGES EVERYTHING declaration on social media, but it’s enough to be less cynical and more optimistic about agentic coding. It’s an invitation to continue creating more difficult tasks for Opus 4.5 to solve. From this point going forward, I will also switch to the terminal Claude Code, since my pipeline is simple enough and doesn’t warrant a UI or other shenanigans. If you’ve spent enough time on programming forums such as Hacker News, you’ve probably seen the name “Rust”, often in the context of snark. Rust is a relatively niche compiled programming language that touts two important features: speed, which is evident in framework benchmarks where it can perform 10x as fast as the fastest Python library, and memory safety enforced at compile time through its ownership and borrowing systems which mitigates many potential problems. For over a decade, the slogan “Rewrite it in Rust” became a meme where advocates argued that everything should be rewritten in Rust due to its benefits, including extremely mature software that’s infeasible to actually rewrite in a different language. Even the major LLM companies are looking to Rust to eke out as much performance as possible: OpenAI President Greg Brockman recently tweeted “rust is a perfect language for agents, given that if it compiles it’s ~correct” which — albeit that statement is silly at a technical level since code can still be logically incorrect — shows that OpenAI is very interested in Rust, and if they’re interested in writing Rust code, they need their LLMs to be able to code well in Rust. I myself am not very proficient in Rust. Rust has a famously excellent interactive tutorial , but a persistent issue with Rust is that there are few resources for those with intermediate knowledge: there’s little between the tutorial and “write an operating system from scratch.” That was around 2020 and I decided to wait and see if the ecosystem corrected this point (in 2026 it has not), but I’ve kept an eye on Hacker News for all the new Rust blog posts and library crates so that one day I too will be able to write the absolutely highest performing code possible. Historically, LLMs have been poor at generating Rust code due to its nicheness relative to Python and JavaScript. Over the years, one of my test cases for evaluating new LLMs was to ask it to write a relatively simple application such as but even without expert Rust knowledge I could tell the outputs were too simple and half-implemented to ever be functional even with additional prompting. However, due to modern LLM postraining paradigms, it’s entirely possible that newer LLMs are specifically RLHF-trained to write better code in Rust despite its relative scarcity. I ran more experiments with Opus 4.5 and using LLMs in Rust on some fun pet projects, and my results were far better than I expected. Here are four such projects: As someone who primarily works in Python, what first caught my attention about Rust is the PyO3 crate: a crate that allows accessing Rust code through Python with all the speed and memory benefits that entails while the Python end-user is none-the-wiser. My first exposure to was the fast tokenizers in Hugging Face tokenizers , but many popular Python libraries now also use this pattern for speed, including orjson , pydantic , and my favorite polars . If agentic LLMs could now write both performant Rust code and leverage the bridge, that would be extremely useful for myself. I decided to start with a very simple project: a project that can take icons from an icon font file such as the ones provided by Font Awesome and render them into images at any arbitrary resolution. I made this exact project in Python in 2021, and it’s very hacky by pulling together several packages and cannot easily be maintained. A better version in Rust with Python bindings is a good way to test Opus 4.5. The very first thing I did was create a for Rust by telling Opus 4.5 to port over the Python rules to Rust semantic equivalents. This worked well enough and had the standard Rust idioms: no to handle lifetimes poorly, no unnecessary , no code, etc. Although I am not a Rust expert and cannot speak that the agent-generated code is idiomatic Rust, none of the Rust code demoed in this blog post has traces of bad Rust code smell. Most importantly, the agent is instructed to call clippy after each major change, which is Rust’s famous linter that helps keep the code clean, and Opus is good about implementing suggestions from its warnings. My up-to-date Rust is available here . With that, I built a gigaprompt to ensure Opus 4.5 accounted for both the original Python implementation and a few new ideas I had, such as supersampling to antialias the output. It completed the assignment in one-shot, accounting for all of the many feature constraints specified. The “Python Jupyter Notebook” notebook command at the end is how I manually tested whether the bridge worked, and it indeed worked like a charm. There was one mistake that’s my fault however: I naively chose the fontdue Rust crate as the renderer because I remember seeing a benchmark showing it was the fastest at text rendering. However, testing large icon generation exposed a flaw: achieves its speed by only partially rendering curves, which is a very big problem for icons, so I followed up: Opus 4.5 used its Web Search tool to confirm the issue is expected with and implemented ab_glyph instead which did fix the curves. icon-to-image is available open-source on GitHub . There were around 10 prompts total adding tweaks and polish, but through all of them Opus 4.5 never failed the assignment as written. Of course, generating icon images in Rust-with-Python-bindings is an order of magnitude faster than my old hacky method, and thanks to the better text rendering and supersampling it also looks much better than the Python equivalent. There’s a secondary pro and con to this pipeline: since the code is compiled, it avoids having to specify as many dependencies in Python itself; in this package’s case, Pillow for image manipulation in Python is optional and the Python package won’t break if Pillow changes its API. The con is that compiling the Rust code into Python wheels is difficult to automate especially for multiple OS targets: fortunately, GitHub provides runner VMs for this pipeline and a little bit of back-and-forth with Opus 4.5 created a GitHub Workflow which runs the build for all target OSes on publish, so there’s no extra effort needed on my end. When I used word clouds in Rust as my test case for LLM Rust knowledge, I had an ulterior motive: I love word clouds. Back in 2019, I open-sourced a Python package titled stylecloud : a package built on top of Python’s word cloud, but with the added ability to add more color gradients and masks based on icons to easily conform it into shapes (sound familiar?) However, stylecloud was hacky and fragile, and a number of features I wanted to add such as non-90-degree word rotation, transparent backgrounds, and SVG output flat-out were not possible to add due to its dependency on Python’s wordcloud / matplotlib , and also the package was really slow. The only way to add the features I wanted was to build something from scratch: Rust fit the bill. The pipeline was very similar to above: ask Opus 4.5 to fulfill a long list of constraints with the addition of Python bindings. But there’s another thing that I wanted to test that would be extremely useful if it worked: WebAssembly (WASM) output with wasm-bindgen . Rust code compiled to WASM allows it to be run in any modern web browser with the speed benefits intact: no dependencies needed, and therefore should be future-proof. However, there’s a problem: I would have to design an interface and I am not a front end person, and I say without hyperbole that for me, designing even a simple HTML/CSS/JS front end for a project is more stressful than training an AI. However, Opus 4.5 is able to take general guidelines and get it into something workable: I first told it to use Pico CSS and vanilla JavaScript and that was enough, but then I had an idea to tell it to use shadcn/ui — a minimalistic design framework normally reserved for Web Components — along with screenshots from that website as examples. That also worked. After more back-and-forth with design nitpicks and more features to add, the package is feature complete. However, it needs some more polish and a more unique design before I can release it, and I got sidetracked by something more impactful… was another Rust stress test I gave to LLMs: command line terminals can’t play audio, right? Turns out, it can with the rodio crate. Given the success so far with Opus 4.5 I decided to make the tasks more difficult: terminals can play sound, but can it compose sound? So I asked Opus 4.5 to create a MIDI composer and playback DAW within a terminal, which worked. Adding features forced me to learn more about how MIDIs and SoundFonts actually work, so it was also educational! miditui is available open-sourced on GitHub , and the prompts used to build it are here . During development I encountered a caveat: Opus 4.5 can’t test or view a terminal output, especially one with unusual functional requirements. But despite being blind, it knew enough about the ratatui terminal framework to implement whatever UI changes I asked. There were a large number of UI bugs that likely were caused by Opus’s inability to create test cases, namely failures to account for scroll offsets resulting in incorrect click locations. As someone who spent 5 years as a black box Software QA Engineer who was unable to review the underlying code, this situation was my specialty. I put my QA skills to work by messing around with , told Opus any errors with occasionally a screenshot, and it was able to fix them easily. I do not believe that these bugs are inherently due to LLM agents being better or worse than humans as humans are most definitely capable of making the same mistakes. Even though I myself am adept at finding the bugs and offering solutions, I don’t believe that I would inherently avoid causing similar bugs were I to code such an interactive app without AI assistance: QA brain is different from software engineering brain. One night — after a glass of wine — I had another idea: one modern trick with ASCII art is the use of Braille unicode characters to allow for very high detail . That reminded me of ball physics simulations, so what about building a full physics simulator also in the terminal? So I asked Opus 4.5 to create a terminal physics simulator with the rapier 2D physics engine and a detailed explanation of the Braille character trick: this time Opus did better and completed it in one-shot, so I spent more time making it colorful and fun . I pessimistically thought the engine would only be able to handle a few hundred balls: instead, the Rust codebase can handle over 10,000 logical balls! I explicitly prompted Opus to make the Colors button have a different color for each letter. ballin is available open-sourced on GitHub , and the prompts used to build it are here . The crate also published a blog post highlighting a major change to its underlying math engine , in its 0.32.0 version so I asked Opus 4.5 to upgrade to that version…and it caused crashes, yet tracing the errors showed it originated with itself. Upgrading to 0.31.0 was fine with no issues: a consequence of only using agentic coding for this workflow is that I cannot construct a minimal reproducible test case to file as a regression bug report or be able to isolate it as a side effect of a new API not well-known by Opus 4.5. The main lesson I learnt from working on these projects is that agents work best when you have approximate knowledge of many things with enough domain expertise to know what should and should not work. Opus 4.5 is good enough to let me finally do side projects where I know precisely what I want but not necessarily how to implement it. These specific projects aren’t the Next Big Thing™ that justifies the existence of an industry taking billions of dollars in venture capital, but they make my life better and since they are open-sourced, hopefully they make someone else’s life better. However, I still wanted to push agents to do more impactful things in an area that might be more worth it. Before I wrote my blog post about how I use LLMs, I wrote a tongue-in-cheek blog post titled Can LLMs write better code if you keep asking them to “write better code”? which is exactly as the name suggests. It was an experiment to determine how LLMs interpret the ambiguous command “write better code”: in this case, it was to prioritize making the code more convoluted with more helpful features, but if instead given commands to optimize the code, it did make the code faster successfully albeit at the cost of significant readability. In software engineering, one of the greatest sins is premature optimization , where you sacrifice code readability and thus maintainability to chase performance gains that slow down development time and may not be worth it. Buuuuuuut with agentic coding, we implicitly accept that our interpretation of the code is fuzzy: could agents iteratively applying optimizations for the sole purpose of minimizing benchmark runtime — and therefore faster code in typical use cases if said benchmarks are representative — now actually be a good idea? People complain about how AI-generated code is slow, but if AI can now reliably generate fast code, that changes the debate. Multiplication and division are too slow for Opus 4.6. As a data scientist, I’ve been frustrated that there haven’t been any impactful new Python data science tools released in the past few years other than . Unsurprisingly, research into AI and LLMs has subsumed traditional DS research, where developments such as text embeddings have had extremely valuable gains for typical data science natural language processing tasks. The traditional machine learning algorithms are still valuable, but no one has invented Gradient Boosted Decision Trees 2: Electric Boogaloo. Additionally, as a data scientist in San Francisco I am legally required to use a MacBook, but there haven’t been data science utilities that actually use the GPU in an Apple Silicon MacBook as they don’t support its Metal API; data science tooling is exclusively in CUDA for NVIDIA GPUs. What if agents could now port these algorithms to a) run on Rust with Python bindings for its speed benefits and b) run on GPUs without complex dependencies? This month, OpenAI announced their Codex app and my coworkers were asking questions. So I downloaded it, and as a test case for the GPT-5.2-Codex (high) model, I asked it to reimplement the UMAP algorithm in Rust. UMAP is a dimensionality reduction technique that can take in a high-dimensional matrix of data and simultaneously cluster and visualize data in lower dimensions. However, it is a very computationally-intensive algorithm and the only tool that can do it quickly is NVIDIA’s cuML which requires CUDA dependency hell. If I can create a UMAP package in Rust that’s superfast with minimal dependencies, that is an massive productivity gain for the type of work I do and can enable fun applications if fast enough. After OpenAI released GPT-5.3-Codex (high) which performed substantially better and faster at these types of tasks than GPT-5.2-Codex, I asked Codex to write a UMAP implementation from scratch in Rust, which at a glance seemed to work and gave reasonable results. I also instructed it to create benchmarks that test a wide variety of representative input matrix sizes. Rust has a popular benchmarking crate in criterion , which outputs the benchmark results in an easy-to-read format, which, most importantly, agents can easily parse. Example output from . At first glance, the benchmarks and their construction looked good (i.e. no cheating) and are much faster than working with UMAP in Python. To further test, I asked the agents to implement additional different useful machine learning algorithms such as HDBSCAN as individual projects, with each repo starting with this 8 prompt plan in sequence: The simultaneous constraints of code quality requirements via , speed requirements with a quantifiable target objective, and an output accuracy/quality requirement, all do succeed at finding meaningful speedups consistently (atleast 2x-3x) Codex 5.3 after optimizing a principal component analysis implementation. I’m not content with only 2-3x speedups: nowadays in order for this agentic code to be meaningful and not just another repo on GitHub, it has to be the fastest implementation possible . In a moment of sarcastic curiosity, I tried to see if Codex and Opus had different approaches to optimizing Rust code by chaining them: This works . From my tests with the algorithms, Codex can often speed up the algorithm by 1.5x-2x, then Opus somehow speeds up that optimized code again to a greater degree. This has been the case of all the Rust code I’ve tested: I also ran the and the word cloud crates through this pipeline and gained 6x cumulative speed increases in both libraries. Can these agent-benchmaxxed implementations actually beat the existing machine learning algorithm libraries, despite those libraries already being written in a low-level language such as C/C++/Fortran? Here are the results on my personal MacBook Pro comparing the CPU benchmarks of the Rust implementations of various computationally intensive ML algorithms to their respective popular implementations, where the agentic Rust results are within similarity tolerance with the battle-tested implementations and Python packages are compared against the Python bindings of the agent-coded Rust packages: I’ll definitely take those results with this unoptimized prompting pipeline! In all cases, the GPU benchmarks are unsurprisingly even better and with wgpu and added WGSL shaders the code runs on Metal without any additional dependencies, however further testing is needed so I can’t report numbers just yet. Although I could push these new libraries to GitHub now, machine learning algorithms are understandably a domain which requires extra care and testing. It would be arrogant to port Python’s scikit-learn — the gold standard of data science and machine learning libraries — to Rust with all the features that implies. But that’s unironically a good idea so I decided to try and do it anyways. With the use of agents, I am now developing (extreme placeholder name), a Rust crate that implements not only the fast implementations of the standard machine learning algorithms such as logistic regression and k-means clustering , but also includes the fast implementations of the algorithms above: the same three step pipeline I describe above still works even with the more simple algorithms to beat scikit-learn’s implementations. This crate can therefore receive Python bindings and even expand to the Web/JavaScript and beyond. This also gives me the oppertunity to add quality-of-life features to resolve grievances I’ve had to work around as a data scientist, such as model serialization and native integration with pandas/polars DataFrames. I hope this use case is considered to be more practical and complex than making a ball physics terminal app. Many people reading this will call bullshit on the performance improvement metrics, and honestly, fair. I too thought the agents would stumble in hilarious ways trying, but they did not. To demonstrate that I am not bullshitting, I also decided to release a more simple Rust-with-Python-bindings project today: nndex, an in-memory vector “store” that is designed to retrieve the exact nearest neighbors as fast as possible (and has fast approximate NN too), and is now available open-sourced on GitHub . This leverages the dot product which is one of the simplest matrix ops and is therefore heavily optimized by existing libraries such as Python’s numpy …and yet after a few optimization passes, it tied even though leverages BLAS libraries for maximum mathematical performance. Naturally, I instructed Opus to also add support for BLAS with more optimization passes and it now is 1-5x numpy’s speed in the single-query case and much faster with batch prediction. 3 It’s so fast that even though I also added GPU support for testing, it’s mostly ineffective below 100k rows due to the GPU dispatch overhead being greater than the actual retrieval speed. Comparison of Python to numpy on test workloads. measures result matches (perfect match) and measure the largest difference between calculated cosine similarities (effectively zero). One of the criticisms about AI generated code is that it “just regurgitates everything on GitHub” but by construction, if the code is faster than what currently exists, then it can’t have been stolen and must be an original approach. Even if the explicit agentic nature of makes it risky to adopt downstream, the learnings from how it accomplishes its extreme speed are still valuable. Like many who have hopped onto the agent train post-Opus 4.5, I’ve become nihilistic over the past few months, but not for the typical reasons. I actually am not hitting burnout and I am not worried that my programming skills are decaying due to agents: on the contrary, the session limits intended to stagger server usage have unintentionally caused me to form a habit of coding for fun an hour every day incorporating and implementing new ideas. However, is there a point to me writing this blog post and working on these libraries if people will likely just reply “tl;dr AI slop” and “it’s vibecoded so it’s automatically bad”? The real annoying thing about Opus 4.6/Codex 5.3 is that it’s impossible to publicly say “Opus 4.5 (and the models that came after it) are an order of magnitude better than coding LLMs released just months before it” without sounding like an AI hype booster clickbaiting, but it’s the counterintuitive truth to my personal frustration. I have been trying to break this damn model by giving it complex tasks that would take me months to do by myself despite my coding pedigree but Opus and Codex keep doing them correctly. On Hacker News I was accused of said clickbaiting when making a similar statement with accusations of “I haven’t had success with Opus 4.5 so you must be lying.” The remedy to this skepticism is to provide more evidence in addition to greater checks and balances, but what can you do if people refuse to believe your evidence? A year ago, I was one of those skeptics who was very suspicious of the agentic hype, but I was willing to change my priors in light of new evidence and experiences, which apparently is rare. Generative AI discourse has become too toxic and its discussions always end the same way, so I have been experimenting with touching grass instead, and it is nice. At this point, if I’m not confident that I can please anyone with my use of AI, then I’ll take solace in just pleasing myself. Continue open sourcing my projects, writing blog posts, and let the pieces fall as they may. If you want to follow along or learn when releases, you can follow me on Bluesky . Moment of introspection aside, I’m not sure what the future holds for agents and generative AI. My use of agents has proven to have significant utility (for myself at the least) and I have more-than-enough high-impact projects in the pipeline to occupy me for a few months. Although certainly I will use LLMs more for coding apps which benefit from this optimization, that doesn’t imply I will use LLMs more elsewhere: I still don’t use LLMs for writing — in fact I have intentionally made my writing voice more sardonic to specifically fend off AI accusations. With respect to Rust, working with agents and seeing how the agents make decisions/diffs has actually helped me break out of the intermediate Rust slog and taught me a lot about the ecosystem by taking on more ambitious projects that required me to research and identify effective tools for modern Rust development. Even though I have technically released Rust packages with many stars on GitHub, I have no intention of putting Rust as a professional skill on my LinkedIn or my résumé. As an aside, how exactly do résumés work in an agentic coding world? Would “wrote many open-source libraries through the use of agentic LLMs which increased the throughput of popular data science/machine learning algorithms by an order of magnitude” be disqualifying to a prospective employer as they may think I’m cheating and faking my expertise? My obligation as a professional coder is to do what works best, especially for open source code that other people will use. Agents are another tool in that toolbox with their own pros and cons. If you’ve had poor experiences with agents before last November, I strongly urge you to give modern agents another shot, especially with an tailored to your specific coding domain and nuances (again here are my Python and Rust files, in conveient copy/paste format). Overall, I’m very sad at the state of agentic discourse but also very excited at its promise: it’s currently unclear which one is the stronger emotion. Two subtle ways agents can implicitly negatively affect the benchmark results but wouldn’t be considered cheating/gaming it are a) implementing a form of caching so the benchmark tests are not independent and b) launching benchmarks in parallel on the same system. I eventually added rules to ideally prevent both.  ↩︎ The crate beat the agent-optimized GBT crate by 4x on my first comparison test, which naturally I took offense: I asked Opus 4.6 to “Optimize the crate such that wins in ALL benchmarks against .” and it did just that.  ↩︎ Currently, only the macOS build has BLAS support as Win/Linux BLAS support is a rabbit hole that needs more time to investigate. On those platforms, numpy does win, but that won’t be the case for long!  ↩︎ Implement the package with the specific functional requirements and design goals; afterwards, create benchmarks with specific matrix sizes that are representative of typical use cases Do a second pass to clean up the code/comments and make further optimizations Scan the crate to find areas of algorithmic weaknesses in extreme cases, and write a sentence for each describing the problem, the potential solution, and quantifying the impact of the solution Leveraging the findings found, optimize the crate such that ALL benchmarks run 60% or quicker (1.4x faster). Use any techniques to do so, and repeat until benchmark performance converges, but don’t game the benchmarks by overfitting on the benchmark inputs alone 1 Create custom tuning profiles that take advantage of the inherent quantities of the input data and CPU thread saturation/scheduling/parallelization to optimize the crate such that ALL benchmarks run 60% or quicker (1.4x faster). You can use the flamegraph crate to help with the profiling Add Python bindings using 0.27.2 and , with relevant package-specific constraints (specifying the version is necessary to ensure compatability with Python 3.10+) Create corresponding benchmarks in Python, and write a comparison script between the Python bindings and an existing Python package Accuse the agent of potentially cheating its algorithm implementation while pursuing its optimizations, so tell it to optimize for the similarity of outputs against a known good implementation (e.g. for a regression task, minimize the mean absolute error in predictions between the two approaches) Instruct Codex to optimize benchmarks to 60% of runtime Instruct Opus to optimize benchmarks to 60% of runtime Instruct Opus to minimize differences between agentic implementation and known good implementation without causing more than a 5% speed regression on any benchmarks UMAP: 2-10x faster than Rust’s fast-umap , 9-30x faster than Python’s umap HDBSCAN (clustering algorithm): 23-100x faster than the hdbscan Rust crate, 3x-10x faster than Python’s hdbscan GBDT (tree-boosting algorithm): 1.1x-1.5x faster fit/predict than the treeboost Rust crate 2 , 24-42x faster fit/1-5x faster predict than Python’s xgboost Two subtle ways agents can implicitly negatively affect the benchmark results but wouldn’t be considered cheating/gaming it are a) implementing a form of caching so the benchmark tests are not independent and b) launching benchmarks in parallel on the same system. I eventually added rules to ideally prevent both.  ↩︎ The crate beat the agent-optimized GBT crate by 4x on my first comparison test, which naturally I took offense: I asked Opus 4.6 to “Optimize the crate such that wins in ALL benchmarks against .” and it did just that.  ↩︎ Currently, only the macOS build has BLAS support as Win/Linux BLAS support is a rabbit hole that needs more time to investigate. On those platforms, numpy does win, but that won’t be the case for long!  ↩︎

0 views
Manuel Moreale 2 weeks ago

Dominik Schwind

This week on the People and Blogs series we have an interview with Dominik Schwind, whose blog can be found at lostfocus.de . Tired of RSS? Read this in your browser or sign up for the newsletter . People and Blogs is supported by the "One a Month" club members. If you enjoy P&B, consider becoming one for as little as 1 dollar a month. My name is Dominik Schwind and I'm from Lörrach , a small town on the German side of the tri-border area with Switzerland and France . I've been a web developer for a really long time now, mostly server-side and just occasionally dabbling in what is showing up in the browser. Annoyingly that's a hobby that I turned into work, so I guess that's ruined now. (Which doesn't stop me, though: I have too many half-finished side-project websites and apps to count.) Besides that I also really like to take photos and after a few years of being frozen in place I started to travel again, which is always nice. I do like watching motorsports of almost all types, I can easily get sucked into computer games like Factorio and I like to listen to podcasts, top of them being the Omnibus Project , Do Go On and Roderick on the Line . I've had a website since before I had internet access - some computer game I had in the mid-90s had the manual included as HTML and I used it to learn how to make basic websites. The very first day my father came home with a modem, I signed up for GeoCities and when I found a webhost that would allow me to run CGI scripts, I installed NewsPro , an early proto-blog system before blogging was even a thing. And while these early iterations of my website(s) are long gone, I haven't stopped since. The name came from an unease I started to feel in my final year of high school: once I finished school, I didn't know where to direct my energy and attention. That feeling hasn't really left since then. Mostly there is none - when I think of something that I want to communicate to someone, anyone , I try to put it online. Quite often it ends up on Mastodon but I do try to put things on my blog, especially when I know it is something future me would appreciate. A few years ago I noticed that I had neglecting my blog in favour of other ways of communicating and I started a pact with a couple of friends to write weeknotes . We're in our fourth year now, which feels like an accomplishment. I try to write those posts first thing on a Sunday morning, if possible. I write most of my posts in Markdown in iA Writer , which is probably the most arrogant Markdown editing app in the world. But I paid for it at some point, so I better use it, too. I basically only need a computer and a place to sit and I'm fine. I've tried to find ways to blog from my phone but in the end, I prefer a proper keyboard and a bigger screen. While I never observed any difference in blogging creativity depending on the physical space, I actually quite enjoy writing in places other than my desk. This one is actually pretty simple: I run WordPress , currently on a DigitalOcean VM. One of the points on my long to-do list for my web stuff is to move it to Hetzner , which probably would only take an evening. And yet, I procrastinate. I've (more or less) jokingly said I'd replace WordPress with a CMS of my own making for years now, but at some point I've resigned, even though my database is a mess. Probably not. Ever since the beginning I wrote for two audiences: my friends and future me. I'm really happy when someone else finds my blog and might turn into an internet friend, but I wouldn't know how else to achieve that other than what I've been doing for all these years now. .de domains are pretty affordable, so it is that plus the server, which is around €100 per year. The blog doesn't generate any revenue, in many ways it's "only" a journal. When it comes to other bloggers, I'd say: go for it if you think your writing (or your photography or whatever it might be you're sharing on your website) is something that can be turned into revenue, one way or another. In many ways I'm a bit bummed that Flattr (or something similar) never really took of, I would happily use a service like that. Of course I need to mention my friends and fellow weeknoters: Martin (blogs in German) and Teymur . (NSFW) Three of the people whose blogs I read have been interviewed here already: Ahn ( Interview ), Jeremy Keith ( Interview ) and Winnie Lim .( Interview ) Some other people whose blogs I read and who might be interesting people to answer your questions would be Jennifer Mills , (who has the best take on weekly blog posts I have ever seen) Nikkin , (he calls it a newsletter, but there is an RSS feed) Roy Tang and Ruben Schade . If you don't have one yet, go start a personal website! Don't take it too seriously, try things and it can be a nice, meditative hobby and helps against the urge to doomscroll. Also you might never know, your kind of people might find it and connect with you. Now that you're done reading the interview, go check the blog and subscribe to the RSS feed . If you're looking for more content, go read one of the previous 130 interviews . People and Blogs is possible because kind people support it.

0 views
fLaMEd fury 2 weeks ago

Making fLaMEd fury Glow Everywhere With an Eleventy Transform

What’s going on, Internet? I originally added a CSS class as a fun way to make my name (fLaMEd) and site name (fLaMEd fury) pop on the homepage. shellsharks gave me a shoutout for it, which inspired me to take it further and apply the effect site-wide. Site wide was the original intent and the problem was it was only being applied manually in a handful of places, and I kept forgetting to add it whenever I wrote a new post or created a new page. Classic. Instead of hunting through templates and markdown files, I’ve added an Eleventy HTML transform that automatically applies the glow up. I had Claude Code help me figure out the regex and the transform config. This allowed me to get this done before the kids came home. Don't @ me. The effect itself is a simple utility class using : Swap for whatever gradient custom property you have defined. The repeats the gradient across the text for a more dynamic flame effect. The transform lives in its own plugin file and gets registered in . It runs after Eleventy has rendered each page, tokenises the HTML by splitting on tags, tracks a skip-tag stack, and only replaces text in text nodes. Tags in the set, along with any span already carrying the class, push onto the stack. No replacement happens while the stack is non-empty, so link text, code examples, the page , and already-wrapped instances are all left alone. HTML attributes like and are never touched because they sit inside tag tokens, not text nodes. A single regex handles everything in one pass. The optional group matches " fury" (with space) or “fury” (without), so “flamed fury” and “flamedfury” (as it appears in the domain name) are both wrapped as a unit. The flag covers every capitalisation variant (“fLaMEd fury”, “Flamed Fury”, “FLAMED FURY”) with the original casing preserved in the output. This helps because I can be inconsistent with the styling at times. Export the plugin from wherever you manage your Eleventy plugins: Then register it in . Register it before any HTML prettify transform so the spans are in place before reformatting runs: That’s it. Any mention of the site name (fLaMEd fury) in body text gets the gradient automatically, in posts, templates, data-driven content, wherever. Look out for the easter egg I’ve dropped in. Later. Hey, thanks for reading this post in your feed reader! Want to chat? Reply by email or add me on XMPP , or send a webmention . Check out the posts archive on the website.

0 views
James Stanley 2 weeks ago

Bot Forensics

Most threat intelligence bots are easy to fingerprint. And trying to be stealthy often makes it worse because imperfect anti-detection methods have extra fingerprint surface area of their own. We run an instrumented honeypot site that collects data on what these bots do, and we've just released an Instant Bot Test so you can see whether we flag your bot without even having to talk to us first. You may want to see my previous post on this topic for more context on what we're doing. Since that post we've sold a handful of reports, including to a couple of big names. And we now have a website at botforensics.com to advertise our services. Anti-detection detection One of the most interesting things we've learnt is that anti-detection techniques are very rarely successful in preventing your bot from being detected. Our collector site sees only an extreme minority (<0.1%) of sessions that could plausibly be real human users. Far from preventing a bot from being detected, anti-detection measures more often provide specific fingerprints about which bot it is based on which measures are in use. Some of these measures take us from "we think this is probably a bot" to "this is bot XYZ operated by Foocorp", which is kind of an own goal. If you're going to run a bot with anti-detection measures in place (and you should, otherwise you'll trivially look like Headless Chrome), then you should definitely get a Bot Audit to make sure you aren't leaking any extra signals. The Puppeteer stealth evasions are a great example of this. Lots of bots are browsing with these evasions applied (we even see bots using them outside Puppeteer), but we can detect the evasions themselves, which often leak more signal than we would expect to see absent the evasions. We do take a canvas fingerprint because why not, but it turns out to be quite hard to definitively say that a given canvas is a bot unless you have enough data on real user sessions to rule out the possibility that it is a real user. While some people are very worried about canvas fingerprinting, a much stronger bot signal than the canvas fingerprint itself is if we read the pixel data out and it has random pixels in the wrong colour where it should be the same colour all over. And, worse, if we do the same thing twice in a row and get a different answer each time! We noticed a bot operated by Microsoft that had some very specific identifying features, including references to some of their developers' real names. Microsoft have a fairly reputable bug bounty programme, so I tested the waters by reporting it on MSRC . But after sitting on it for 2 weeks they classified it as "not important" and declined to pay a bounty, so I won't make this mistake again. To Microsoft's credit, they have still not fixed it, which is consistent with considering it not important. We are in some cases able to detect when bots are running on Kubernetes (thanks Feroz for the idea), and this also reveals some fingerprints that are unique to each Kubernetes cluster. This is a great signal because a.) hardly any real human users are browsing from inside Kubernetes, and b.) if 2 bots are running on the same Kubernetes cluster then it's a fair bet that they're operated by the same company. So far we have seen bots from 3 distinct Kubernetes clusters. We've been surprised by how few threat intelligence vendors are running their own fetching. There are 94 vendors listed on VirusTotal, but fewer than 50 genuinely distinct bots fetch our collector pages, so at most only a bit over half of those vendors are actually fetching the sites themselves. The others may outsource their fetching to a common third-party, or else they are simply consulting other threat intelligence vendors and not even doing classification themselves. If you looked at enough VirusTotal results pages you could probably work out which ones always share the same classification, maybe we should do that. One of our domains is now blocked on VirusTotal by 7 different vendors: This is kind of a poor show. You can't classify a site as phishing just because it has "bank" in the domain and the page has a login form. The litmus test for whether a site is phishing is whether you can name the site it is impersonating, and our collector site doesn't impersonate any real site. Vexatious takedowns We received our first takedown notices last week. To be honest, I expected this to happen sooner. The whole project is running on "disposable" infrastructure so that if it gets taken down it won't impact any of our other projects. But it would still be very inconvenient to have it taken down. The takedown notices were sent to our hosting provider, who forwarded them to us. It's possible they were also sent to our domain registrar, who did not forward them to us but also did not act on them. Here's the text from the first one: Hello, We have discovered a Phishing attack on your network. URL: hxxps[:]// REDACTED / IP's: REDACTED Threat Type: Phishing Threat Description: Banking credential harvesting page detected at REDACTED . The page presents a fake bank login form with a header that references BotForensics Collector Page and botforensics .com, which indicates branding inconsistent with any legitimate bank . The site is hosted on REDACTED infrastructure (IP REDACTED ) and registered recently on 2026-02-17 via REDACTED , with privacy-protected WHOIS data . The HTML shows a typical login card for username and password, a Sign In” [sic] button, and scripted UI enhancements, including external scripts and images, plus a dynamic header bar . This combination is characteristic of a phishing attempt intended to harvest user credentials . The domain age is only about 0 .01 years, and the presence of a login form on a brand-tampering page hosted on a known hosting provider strongly suggests malicious intent . Registrar abuse contact is abuse[@] REDACTED and hosting provider abuse contact is abuse[@] REDACTED . Because high confidence phishing has been detected, the page should be reported to abuse contacts and blocked; while there can be legitimate educational use of such content, the page as presented is designed to harvest credentials rather than serve legitimate banking functionality . Domain Registrar: REDACTED ASN: REDACTED This email was sent automatically by QuariShield Automated Analysis. Reports are sometimes verified using AI, while this means reports are mostly valid, there may be some false positives. For more info: REDACTED We are well aware that you may not be able to take abuse reports sent to this email address, therefore if you could forward this email to the correct team who can handle abuse reports, it would be much appreciated. Please note, replies to this email are logged, but aren't always seen, we don't usually monitor this email for replies. To contact us if you have any questions or concerns, please email [email protected] stating your Issue ID REDACTED Kind regards, QuariShield Cyber Security. (Redactions mine, but yes the text is all run into one like that with no linebreaks). A few highlights stand out: The page presents a fake bank login form with a header that references BotForensics Collector Page and botforensics .com, which indicates branding inconsistent with any legitimate bank . One would think that having branding "inconsistent with any legitimate bank" is evidence that you're not phishing? A phishing site would copy the bank's branding. The HTML shows a typical login card for username and password, a Sign In” button, and scripted UI enhancements, including external scripts and images, plus a dynamic header bar . This combination is characteristic of a phishing attempt intended to harvest user credentials Is it really? hosted on a known hosting provider What are the chances? This email was sent automatically by QuariShield Automated Analysis. Reports are sometimes verified using AI Very interesting. The takedown notices were sent by QuariShield . I emailed the QuariShield contact address and got a reply from the person operating it, and he seems friendly, and has whitelisted my collector page, which is helpful but in my opinion only part of the solution. How many other false positive takedown notices is he going to send for other websites? From what I have been able to gather, QuariShield grabs URLs from public sources, and uses an LLM agent to classify them and automatically send takedowns. On the one hand, yeah, it's not working very well yet and has a lot of false positives. On the other hand, just look at how far we've come. If you're running a traditional takedown provider: this is what's coming for you. People are spinning up (presumed) vibe-coded projects that now do fully-automated takedowns for sites that aren't even paying customers . Your anti-detection techniques may not be as effective as you think. Try our Instant Bot Test to see if we flag your bot (and please let us know how we did). And the lesson from QuariShield is: AI is coming for you.

0 views
Evan Schwartz 2 weeks ago

Great RSS Feeds That Are Too Noisy to Read Manually

Some RSS feeds are fantastic but far too noisy to add to most RSS readers directly. Without serious filtering, you'd get swamped with more posts than you could possibly read, while missing the hidden gems. I built Scour specifically because I wanted to find the great articles I was missing in noisy feeds like these, without feeling like I was drowning in unread posts. If you want to try it, you can add all of these sources in one click . But these feeds are worth knowing about regardless of what reader you use. Feed: https://hnrss.org/newest Thousands of posts are submitted to Hacker News each week. While the front page gives a sense of what matches the tech zeitgeist, there are plenty of interesting posts that get buried simply because of the randomness of who happens to be reading the Newest page and voting in the ~20 minutes after posts are submitted. (You can try searching posts that were submitted but never made the front page in this demo I built into the Scour docs.) Feed: https://feeds.pinboard.in/rss/recent/ Pinboard describes itself as "Social Bookmarking for Introverts". The recent page is a delightfully random collection of everything one of the 30,000+ users has bookmarked. Human curated, without curation actually being the goal. Feed: https://bearblog.dev/discover/feed/?newest=True Bear is "A privacy-first, no-nonsense, super-fast blogging platform". This post is published on it, and I'm a big fan. The Discovery feed gives a snapshot of blogs that users have upvoted on the platform. But, even better than that, the Most Recent feed gives you every post published on it. There are lots of great articles, and plenty of blogs that are just getting started. Feed: https://feedle.world/rss Feedle is a search engine for blogs and podcasts. You can search for words or phrases among their curated collection of blogs, and every search can become an RSS feed. An empty search will give you a feed of every post published by any one of their blogs. Feed: https://kagi.com/api/v1/smallweb/feed/ Kagi, the search engine, maintains an open source list of around 30,000 "small web" websites that are personal and non-commercial sites. Their Small Web browser lets you browse random posts one at a time. The RSS feed gives you every post published by any one of those websites. Feed: https://threadreaderapp.com/rss.xml Thread Reader is a Twitter/X bot that lets users "unroll" threads into an easier-to-read format. While getting RSS feeds out of Twitter/X content is notoriously difficult, Thread Reader provides an RSS feed of all threads that users have used them to unroll. Like the content on that platform, the threads are very hit-or-miss, but there are some gems in there. Not an RSS feed: https://minifeed.net/global Minifeed is a nice "curated blog reader and search engine". They have a Global page that shows every post published by one of the blogs they've indexed. While this isn't technically an RSS feed, I thought it deserved a mention. Note that Scour can add some websites that don't have RSS feeds. It treats pages with repeated structures that look like blogs (e.g. they have links, titles, and publish dates) as if they were RSS feeds. Minifeed's Global view is one such page, so you can also get every post published from any one of their collected blogs. Feeds galore: https://info.arxiv.org/help/rss.html arXiv has preprint academic articles for technical fields ranging from Computer Science and Mathematics to Physics and Quantitative Biology. Like many of the feeds listed above, most of the categories are very noisy. But, if you're into reading academic articles, there is also plenty of great new research hidden in the noise. Every field and sub-field has its own RSS feed. (You can browse them and subscribe on Scour here ). While reading my Scour feed, I'll often check which feeds an article I liked came from (see what this looks like here ), and I'm especially delighted when it comes from some source I had no idea existed. These types of noisy feeds are great ways of discovering new content and new blogs, but you definitely need some good filters to make use of them. I hope you'll give Scour a try! P.S. Scour makes all of the feeds it creates consumable as RSS/Atom/JSON feeds , so you can add your personalized feed or each of your interests-specific feeds to your favorite feed reader. Read more in this guide for RSS users .

0 views
Andre Garzia 2 weeks ago

Building your own blogging tools is a fun journey

# Building your own blogging tools is a fun journey I read a very interesting blog post today: ["So I've Been Thinking About Static Site Generators" by PolyWolf](https://wolfgirl.dev/blog/2026-02-23-so-ive-been-thinking-about-static-site-generators/) in which she goes in depth about her quest to create a 🚀BLAZING🔥 fast [static site generator](https://en.wikipedia.org/wiki/Static_site_generator). It was a very good read and I'm amazed at how fast she got things running. The [conversation about the post on Lobste.rs](https://lobste.rs/s/pgh4ss/so_i_ve_been_thinking_about_static_site) is also full of gems. Seeing so many people pouring energy into the specific problem of making SSGs very fast feels to me pretty much like modders getting the utmost performance out of their CPUs or car engines. It is fun to see how they are doing and how fast they can make clean and incremental builds go. > No one will ever complain about their SSG being too fast. As someone who used [a very slow SSG](https://docs.racket-lang.org/pollen/) for years and eventually migrated to [my own homegrown dynamic site](/2025/03/why-i-choose-lua-for-this-blog.html), I understand how frustrating slow site generation can be. In my own personal case, I decided to go with an old-school dynamic website using old 90s tech such as *cgi-bin* scripts in [Lua](https://lua.org). That eliminates the need for rebuilds of the site as it is generated at runtime. One criticism I keep hearing is about the scalability of my approach, people say: *"what if one of your posts go viral and the site crashes?"*, well, that is ok for me cause if I get a cold or flu I crash too, why would I demand of my site something I don't demand of myself? Jokes aside, the problem of scalability can be dealt with by having some heuristic figuring out when a post is getting hot and then generating a static version of that post while keeping posts that are not hot dynamic. I'm not worried about it. Instead of devoting my time to the engineering problem of making my SSG fast, I decided to put my energy elsewhere. A point that is often overlooked by many people developing blogging systems is the editing and posting workflow. They'll have really fast SSGs and then let the user figure out how to write the source files using whatever tool they want. Nothing wrong with that, but I want something better than launching $EDITOR to write my posts. In my case, what prevented me from posting more was not how long my SSG took to rebuild my site, but the friction between wanting to post and having the post written. What tools to use, how to handle file uploads, etc. So I begun to optmising and developing tools for helping me with that. First, I [made a simple posting interface](/2025/01/creating-a-simple-posting-interface.html). This is not a part of the blogging system, it is an independent tool that shares the code base with the rest of the blog (just so I have my own CGI routines available). Internally it uses [micropub](https://www.w3.org/TR/micropub/) to publish. After that, I made it into a Firefox Add-on. The add-on is built for ad-hoc distribution and not shared on the store, it is just for me. Once installed, I get a sidebar that allows me to edit or post. ![Editor](/2026/02/img/c6e6afba-3141-4eca-a0f4-3425f7bea0d8.png) This is part of making my web browser of choice not only a web browser but a web making tool. I'm integrating all I need to write posts into the browser itself and thus diminishing the distance between browsing the web and making the web. I added features to the add-on to help me quote posts, get addresses as I browse them, and edit my own posts. It is all there right in the browser. ![quoting a post](/2026/02/img/b6d2abc1-c4ae-42c5-931f-d390fc9b793f.png) Like PolyWolf, I am passionate about my tools and blogging. I think we should take upon ourselves to build the tools we need if they're not available already (or just for the fun of it). Even though I'm no longer in the SSG bandwagon anymore, I'm deeply interested in blogging and would like to see more people experimenting with building their own tools, especially if their focus is on interesting ux and writing workflows.

0 views
Thomasorus 2 weeks ago

Is frontend development a dead-end career?

It's been almost a year at my current job. I was hired as a frontend developer and UI/UX coordinator, but I've been slowly shifting to a project manager role, which I enjoy a lot and where I think I contribute more. We build enterprise web apps, the kinds you will never see online, never hear about, but that power entire companies. The config application for the car assembly line, the bank counsellor website that outputs your mortgage rate, the warehouse inventory systems... That's the kind of thing we do. For backend engineers that's a very interesting job. You get to understand how entire professional fields work, and try to reproduce them in code. But for us the frontend guys? The interest is, limited to put it simply. We're talking about pages with multi-step conditional forms, tables with lots of columns and filters, 20 nuances of inputs, modals... The real challenge isn't building an innovative UI or UX, it's maintaining a consistency in project that can last years and go into the hands of multiple developers. Hence my UI/UX coordinator role where I look at my colleagues work and sternly say "That margin should be , not " . Because here's the thing: this type of client doesn't care if it's pretty or not and won't pay for design system work or maintenance. To them, stock Bootstrap or Material Design is amazingly beautiful compared to their current Windev application. What they want is stability and predictability, they care it works the same when they encounter the same interface. Sometimes, if a process is too complex for new hires, they will engage into talks to make it more user friendly, but that's it. Until recently, the only generated code we used were the types for TypeScript and API calls functions generated from the backend, which saved us a lot of repetitive work. We made experiments with generative AI and found out we could generate a lot of our template code. All that's left to do is connect both, the front of the frontend and the back of the frontend , mostly click events, stores, reactivity, and so on. People will say that's where the fun is, and sometimes yes, I agree. I've been on projects where building the state was basically building a complex state machine out of dozens of calls from vendor specific APIs. But how often do you do that? And why would you do that if you are developing the backend yourself and can pop an endpoint with all the data your frontend needs? And so I've been wondering about the future. With frameworks, component libraries, LLMs, the recession pushing to deliver fast even if mediocre code and features, who needs someone who can write HTML, CSS, JS? Who can pay for the craft of web development? Are the common frontend developers folks, not the already installed elite freelancers building websites for prestigious clients , only destined to do assembly line of components by prompting LLMs before putting some glue between code blocks they didn't write?

0 views
Preah's Website 2 weeks ago

Learning Java Again

Java was my first programming language I learned, it’s my baby. Well also HTML and CSS. But for scripting, I’ve always enjoyed writing Java code. I’ve become pretty familiar with Python at this point, and haven’t touched Java in ages, but really feel the itch to pick it up seriously again since it is what taught me programming and computer science concepts to begin with. I actually still recommend Java as a first programming language over Python, since it touches a lot of concepts that I think are good to start with from the beginning. It’s easier to move to Python than to move to Java or C++ from Python. Anyone have project ideas or recommendations for writing more Java? Let me know :) Subscribe via email or RSS

1 views
Jim Nielsen 2 weeks ago

Making Icon Sets Easy With Web Origami

Over the years, I’ve used different icon sets on my blog. Right now I use Heroicons . The recommended way to use them is to copy/paste the source from the website directly into your HTML. It’s a pretty straightforward process: If you’re using React or Vue, there are also npm packages you can install so you can import the icons as components. But I’m not using either of those frameworks, so I need the raw SVGs and there’s no for those so I have to manually grab the ones I want. In the past, my approach has been to copy the SVGs into individual files in my project, like: Then I have a “component” for reading those icons from disk which I use in my template files to inline the SVGs in my HTML. For example: It’s fine. It works. It’s a lot of node boilerplate to read files from disk. But changing icons is a bit of a pain. I have to find new SVGs, overwrite my existing ones, re-commit them to source control, etc. I suppose it would be nice if I could just and get the raw SVGs installed into my folder and then I could read those. But that has its own set of trade-offs. For example: So the project’s npm packages don’t provide the raw SVGs. The website does, but I want a more programatic way to easily grab the icons I want. How can I do this? I’m using Web Origami for my blog which makes it easy to map icons I use in my templates to Heroicons hosted on Github. It doesn’t require an or a . Here’s an snippet of my file: As you can see, I name my icon (e.g. ) and then I point it to the SVG as hosted on Github via the Heroicons repo. Origami takes care of fetching the icons over the network and caching them in-memory. Beautiful, isn’t it? It kind of reminds me of import maps where you can map a bare module specifier to a URL (and Deno’s semi-abandoned HTTP imports which were beautiful in their own right). Origami makes file paths first-class citizens of the language — even “remote” file paths — so it’s very simple to create a single file that maps your icon names in a codebase to someone else’s icon names from a set, whether those are being installed on disk via npm or fetched over the internet. To simplify my example earlier, I can have a file like : Then I can reference those icons in my templates like this: Easy-peasy! And when I want to change icons, I simply update the entries in to point somewhere else — at a remote or local path. And if you really want to go the extra mile, you can use Origami’s caching feature: Rather than just caching the files in memory, this will cache them to a local folder like this: Which is really cool because now when I run my site locally I have a folder of SVG files cached locally that I can look at and explore (useful for debugging, etc.) This makes vendoring really easy if I want to put these in my project under source control. Just run the file once and boom, they’re on disk! There’s something really appealing to me about this. I think it’s because it feels very “webby” — akin to the same reasons I liked HTTP imports in Deno. You declare your dependencies with URLs, then they’re fetched over the network and become available to the rest of your code. No package manager middleman introducing extra complexity like versioning, transitive dependencies, install bloat, etc. What’s cool about Origami is that handling icons like this isn’t a “feature” of the language. It’s an outcome of the expressiveness of the language. In some frameworks, this kind of problem would require a special feature (that’s why you have special npm packages for implementations of Heroicons in frameworks like react and vue). But because of the way Origami is crafted as a tool, it sort of pushes you towards crafting solutions in the same manner as you would with web-based technologies (HTML/CSS/JS). It helps you speak “web platform” rather than some other abstraction on top of it. I like that. Reply via: Email · Mastodon · Bluesky Go to the website Search for the icon you want Click to “Copy SVG” Go back to your IDE and paste it Names are different between icon packs, so when you switch, names don’t match. For example, an icon might be named in one pack and in another. So changing sets requires going through all your templates and updating references. Icon packs are often quite large and you only need a subset. might install hundreds or even thousands of icons I don’t need.

0 views
Simon Willison 3 weeks ago

Adding TILs, releases, museums, tools and research to my blog

I've been wanting to add indications of my various other online activities to my blog for a while now. I just turned on a new feature I'm calling "beats" (after story beats, naming this was hard!) which adds five new types of content to my site, all corresponding to activity elsewhere. Here's what beats look like: Those three are from the 30th December 2025 archive page. Beats are little inline links with badges that fit into different content timeline views around my site, including the homepage, search and archive pages. There are currently five types of beats: That's five different custom integrations to pull in all of that data. The good news is that this kind of integration project is the kind of thing that coding agents really excel at. I knocked most of the feature out in a single morning while working in parallel on various other things. I didn't have a useful structured feed of my Research projects, and it didn't matter because I gave Claude Code a link to the raw Markdown README that lists them all and it spun up a parser regex . Since I'm responsible for both the source and the destination I'm fine with a brittle solution that would be too risky against a source that I don't control myself. Claude also handled all of the potentially tedious UI integration work with my site, making sure the new content worked on all of my different page types and was handled correctly by my faceted search engine . I actually prototyped the initial concept for beats in regular Claude - not Claude Code - taking advantage of the fact that it can clone public repos from GitHub these days. I started with: And then later in the brainstorming session said: After some iteration we got to this artifact mockup , which was enough to convince me that the concept had legs and was worth handing over to full Claude Code for web to implement. If you want to see how the rest of the build played out the most interesting PRs are Beats #592 which implemented the core feature and Add Museums Beat importer #595 which added the Museums content type. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Releases are GitHub releases of my many different open source projects, imported from this JSON file that was constructed by GitHub Actions . TILs are the posts from my TIL blog , imported using a SQL query over JSON and HTTP against the Datasette instance powering that site. Museums are new posts on my niche-museums.com blog, imported from this custom JSON feed . Tools are HTML and JavaScript tools I've vibe-coded on my tools.simonwillison.net site, as described in Useful patterns for building HTML tools . Research is for AI-generated research projects, hosted in my simonw/research repo and described in Code research projects with async coding agents like Claude Code and Codex .

0 views