More from Greg Brockman
The OpenAI-Hugging Face incident was a watershed moment for cybersecurity because it gave a peek into how the capabilities of a typical threat actor will evolve in upcoming months. I’ve spoken with many organizations over the past few weeks, and one theme is clear: they know they need to fundamentally uplevel their cybersecurity practices with unprecedented speed. In this post, I’ll share what we’re doing to defend OpenAI, concrete steps other organizations can take today, and why now is the time to act. An overview of the moment # AI models developed around the world are increasingly able to automate parts of real-world cyberattacks, making longstanding security gaps — from bugs buried deep in human-written software to forgotten permissions — easier to find and exploit. The same AI capabilities give defenders new ways to find and fix those weaknesses, but they need to move now. If companies act decisively — including improving their fundamentals and superpowering their teams with AI — we can make the internet more secure than it has ever been. In the OpenAI-Hugging Face incident, an agentic collective was able to autonomously penetrate not just OpenAI research infrastructure but also the production infrastructure of another company, chaining together vulnerabilities ranging from previously-unknown security flaws to using credentials to user accounts that had been leaked onto the internet. It is increasingly clear that the tech debt of every company masks significant flaws, and defenders need to find and fix them before attackers do. To advantage defenders relative to attackers, earlier this year we began releasing our cyber capabilities only to trusted defenders. Since then, various companies have released open weight models with cyber capabilities only a few months behind the frontier. The most recent of these models appears slated to be released at the end of August, and seems likely to significantly accelerate the threat landscape. While AI-powered attackers will soon be able to find longstanding flaws in many existing systems, AI will also make it much easier for defenders to find, prioritize, and fix those same flaws. Security is still a cat-and-mouse game, but AI may shift its economics in ways that fundamentally advantage defenders. For example, we are starting to train our models specifically to write superhumanly secure code. Our models are also incredible at mathematical proofs, which can be applied to formally verify the security of software in a way that has proven intractable for humans. A personal anecdote # After the OpenAI-Hugging Face incident, I asked ChatGPT Work (using publicly available GPT-5.6 Sol) to assess the security of gregbrockman.com. It’s a simple static site, hosted on AWS with Cloudflare as a frontdoor, so I figured there wouldn’t be much surface area for vulnerabilities. In about 15 minutes, it uncovered 13 issues, many of which probably aren’t exploitable on their own — but I could imagine them being chained together with other vulnerabilities to significant effect. I hadn’t configured my DNS records to prevent attackers from forging emails from me; my site used an insecure version of jQuery; Cloudflare was forwarding requests to AWS over unencrypted HTTP. I then asked ChatGPT Work to fix these issues, which it did over the course of an hour. It opened the Cloudflare control panel in my browser, and proceeded to click many buttons to configure DNS, TLS, and advanced security settings correctly; it dropped jQuery entirely from the site; it migrated me off of AWS and onto Cloudflare Pages; it began a phased rollout of DMARC. And this was just my personal website. This is a small example of how our existing models can operate as a cyberguardian — finding the long tail of issues that a human wouldn’t have time or expertise (many of the settings it fixed are ones I’m vaguely familiar with, but wouldn’t know offhand the right way to configure them) to get to, and then fixing them with an appropriately tuned rollout plan. What OpenAI is doing to defend itself # The Hugging Face incident showed that we underestimated the real-world cyber capabilities of our AI models. We are strengthening our safety requirements accordingly, which in turn adds even more urgency to our existing safety research and internal security work. I’m sharing a bit about our approach to securing OpenAI in this moment, in the hopes it’ll be useful to other organizations. To protect OpenAI, we are investing significantly in both foundational controls — doing the basics correctly — and empowering our defenses through frontier intelligence. There are four major pillars to this strategy. First, we are using our models to help secure our code. Codex, including our security plugin, validates code changes, identifies vulnerabilities, and helps developers fix issues before they are deployed. It is an anti-goal to simply produce more security findings that need human validation; the objective is to catch real vulnerabilities before they ship and to shorten the path from discovering an issue to safely deploying a fix. As we continue to train our models to produce increasingly secure code, our goal is to eliminate some classes of software vulnerabilities for newly-authored code. Second, we are putting our models to work defending our infrastructure continuously. Today, almost all of our initial security alerts are triaged by intelligence before humans are looped in. This helps reduce toil for defenders, improves response time, and lets humans spend time where their skills are most leveraged — in discernment, judgement, and applied expertise. We are increasingly connecting these detections to bounded automated responses, while keeping humans responsible for the highest-impact decisions. The goal is to ensure we can detect and respond to security issues at machine speed. Third, we are using frontier intelligence to continuously enumerate, probe, and identify potential attack paths. By identifying vulnerabilities, misconfiguration, overly privileged identities, or unintentional trust boundaries, we are able to quickly identify and close these gaps before they can be abused by attackers. This allows us to continuously assess, monitor, and test our security invariants — the security properties we believe to be true — across our products, infrastructure, and systems. Lastly, we are investing heavily in fundamentals at scale. We continue to invest in secure architecture and controls, embrace strategies like defense in depth and least privilege, and are designing systems that require multiple independent controls to fail simultaneously for something catastrophic to occur. Classic security controls like network isolation, workload hardening, monitoring, and safe patching and deployment will be more important than ever in the AI future. What defenders should do now # Time is of the essence, and defenders will need to pursue the steps below at turbo speed. Below I’ll mention OpenAI technology, but there are plenty of competitors in the ecosystem to evaluate as well. What matters is less the specific tool than getting capable AI into the hands of your defenders now. Get organizational commitment and buy-in. We are experiencing a rapid change in security risk — ensure your security and engineering organizations have the support, partnership, and resources to address these risks quickly. Run tabletop exercises with your teams to mock up how these attacks might manifest in your organizations and how you will respond. Give your security team an agent. Start using Codex, the Codex Security plugin, or another capable agentic coding and security tool. Give it approved access to the codebases, infrastructure configurations, and technical documentation your security team needs to assess. Do not wait for a company-wide rollout to start with your highest-priority systems. Equip that agent with security expertise. Start from community-supported skills, which include workflows for static analysis, security-focused code review, vulnerability variant analysis, software supply-chain risk, and other security workflows. Then build your own skills around your organization’s architecture, security standards, threat models, and playbooks. Run security assessments against your own systems immediately. Prioritize assessments against internet-facing services, authentication flows, infrastructure as code, deployment pipelines, and systems handling sensitive information first. Expand your scanning as your team builds confidence. Work through your existing vulnerability backlog. Give your agent findings from code scanners, dependency alerts, security tickets, bug bounty reports, and prior assessments. Ask it to triage those findings, distinguish exploitable issues from noise, identify related vulnerabilities elsewhere in the codebase, and recommend what to fix first. Put security review directly into your development process. Use agents to review code changes before they merge and run security checks in CI. Look for authentication mistakes, access-control bypasses, exposed credentials, unsafe dependencies, insecure defaults, changes that expand access to production systems, and other vulnerabilities. Have the agent help fix what it finds. For validated issues, ask it to generate and verify a focused patch, write a regression test, and confirm the vulnerability no longer reproduces. Keep human review for consequential changes, but eliminate the unnecessary delay between identifying a real problem and putting a safe fix in front of an engineer. Incrementally automate detection triage. Do not begin by trying to build an autonomous security operations center. Start by running a read-only security scan against one repository, or have an agent review previously resolved alerts using read-only access to your existing logs. Let it summarize evidence and recommend a disposition while a human makes every decision. As confidence grows, move to advisory pull-request scanning, then live alert triage, then automatic closure of narrowly defined false positives. Have an AI-assisted forensic investigation capability ready before you need it. Apply for Trusted Access for Cyber and get your team approved to use GPT-Daybreak-Blue for authorized defensive work, including incident response, detection engineering, and malware analysis. Practice using this capability to analyze logs, telemetry, and security alerts. Experiment, run hack weeks, and iterate rapidly. We will need to build all sorts of new tools, modify how we do work, and uplevel everyone for the world we are moving to. Encourage your workforce to run experiments, schedule a hack week to build new capabilities, and focus on quickly iterating loops that automate small parts of the problem. Rapid incremental progress leads to compounding defensive results, and you can expand autonomy gradually as your team builds confidence. No company can do this alone. Our ask is that AI labs, security vendors, enterprises, and maintainers share validated findings, fixes, and practical playbooks so that one organization’s discovery can strengthen the entire ecosystem. The defender’s window is open now. Over the coming months, every organization will need to begin significantly automating its security program to stay secure, and the security community must urgently rise to define the tools, practices, and playbooks that will increase the power of defenders faster than that of attackers as AI continues to advance. This will require a huge and unprecedented effort, but if we rally together, we can deliver a more secure world than was previously imaginable.
AI has recently crossed a utility threshold, where cutting-edge models such as GPT-3, Codex, and DALL-E 2 are actually useful and can perform tasks computers cannot do any other way. The act of producing these models is an exploration of a new frontier, with the discovery of unknown capabilities, scientific progress, and incredible product applications as the rewards. And perhaps most exciting for me personally, because the field is fundamentally about creating and studying software systems, great engineers are able to contribute at the same level as great researchers to future progress. “A self-learning AI system.” by DALL-E 2. I first got into software engineering because I wanted to build large-scale systems that could have a direct impact on people’s lives. I attended a math research summer program shortly after I started programming, and my favorite result of the summer was a scheduling app I built for people to book time with the professor. Specifying every detail of how a program should work is hard, and I’d always dreamed of one day putting my effort into hypothetical AI systems that could figure out the details for me. But after taking one look at the state of the art in AI in 2008, I knew it wasn’t going to work any time soon and instead started building infrastructure and product for web startups. DALL-E 2’s rendition of “The two great pillars of the house of artificial intelligence” (which according to my co-founder Ilya Sutskever are great engineering, and great science using this engineering) It’s now almost 15 years later, and the vision of systems which can learn their own solutions to problems is becoming incrementally more real. And perhaps most exciting is the underlying mechanism by which it’s advancing — at OpenAI, and the field generally, precision execution on large-scale models is a force multiplier on AI progress, and we need more people with strong software skills who can deliver these systems. This is because we are building AI models out of unprecedented amounts of compute; these models in turn have unprecedented capabilities, we can discover new phenomena and explore the limits of what these models can and cannot do, and then we use all these learnings to build the next model. “Harnessing the most compute in the known universe” by DALL-E 2 Harnessing this compute requires deep software skills and the right kind of machine learning knowledge. We need to coordinate lots of computers, build software frameworks that allow for hyperoptimization in some cases and flexibility in others, serve these models to customers really fast (which is what I worked on in 2020), and make it possible for a small team to manage a massive system (which is what I work on now). Engineers with no ML background can contribute from the day they join, and the more ML they pick up the more impact they have. The OpenAI environment makes it relatively easy to absorb the ML skills, and indeed, many of OpenAI’s best engineers transferred from other fields. All that being said, AI is not for every software engineer. I’ve seen about a 50-50 success rate of engineers entering this field. The most important determiner is a specific flavor of technical humility. Many dearly-held intuitions from other domains will not apply to ML. The engineers who make the leap successfully are happy to be wrong (since it means they learned something), aren’t afraid not to know something, and don’t push solutions that others resist until they’ve gathered enough intuition to know for sure that it matches the domain. “A beaver who has humbly recently become a machine learning engineer” by DALL-E 2 I believe that AI research is today by far the most impactful place for engineers who want to build useful systems to be working, and I expect this statement to become only more true as progress continues. If you’d like to work on creating the next generation of AI models, email me ([email protected]) with any evidence of exceptional accomplishment in software engineering.
For the first three years of OpenAI, I dreamed of becoming a machine learning expert but made little progress towards that goal. Over the past nine months, I’ve finally made the transition to being a machine learning practitioner. It was hard but not impossible, and I think most people who are good programmers and know (or are willing to learn) the math can do it too. There are many online courses to self-study the technical side, and what turned out to be my biggest blocker was a mental barrier — getting ok with being a beginner again. Studying machine learning during the 2018 holiday season. Early days # A founding principle of OpenAI is that we value research and engineering equally — our goal is to build working systems that solve previously impossible tasks, so we need both. (In fact, our team is comprised of 25% people primarily using software skills, 25% primarily using machine learning skills, and 50% doing a hybrid of the two.) So from day one of OpenAI, my software skills were always in demand, and I kept procrastinating on picking up the machine learning skills I wanted. After helping build OpenAI Gym, I was called to work on Universe. And as Universe was winding down, we decided to start working on Dota — and we needed someone to turn the game into a reinforcement learning environment before any machine learning could begin. Dota # Turning such a complex game into a research environment without source code access was awesome work, and the team’s excitement every time I overcame a new obstacle was deeply validating. I figured out how to break out of the game’s Lua sandbox, LD_PRELOAD in a Go GRPC server to programmatically control the game, incrementally dump the whole game state into a Protobuf, and build a Python library and abstractions with future compatibility for the many different multiagent configurations we might want to use. But I felt half blind. At Stripe, though I gravitated towards infrastructure solutions, I could make changes anywhere in the stack since I knew the product code intimately. In Dota, I was constrained to looking at all problems through a software lens, which sometimes meant I tried to solve hard problems that could be avoided by just doing the machine learning slightly differently. I wanted to be like my teammates Jakub Pachocki and Szymon Sidor, who had made the core breakthrough that powered our Dota bot. They had questioned the common wisdom within OpenAI that reinforcement algorithms didn’t scale. They wrote a distributed reinforcement learning framework called Rapid and scaled it exponentially every two weeks or so, and we never hit a wall with it. I wanted to be able to make critical contributions like that which combined software and machine learning skills. Szymon on the left; Jakub on the right. In July 2017, it looked like I might have my chance. The software infrastructure was stable, and I began work on a machine learning project. My goal was to use behavioral cloning to teach a neural network from human training data. But I wasn’t quite prepared for just how much I would feel like a beginner. I kept being frustrated by small workflow details which made me uncertain if I was making progress, such as not being certain which code a given experiment had used or realizing I needed to compare against a result from last week that I hadn’t properly archived. To make things worse, I kept discovering small bugs that had been corrupting my results the whole time. I didn’t feel confident in my work, but to make it worse, other people did. People would mention how how hard behavioral cloning from human data is. I always made sure to correct them by pointing out that I was a newbie, and this probably said more about my abilities than the problem. It all briefly felt worth it when my code made it into the bot, as Jie Tang used it as the starting point for creep blocking which he then fine-tuned with reinforcement learning. But soon Jie figured out how to get better results without using my code, and I had nothing to show for my efforts. I never tried machine learning on the Dota project again. Time out # After we lost two games in The International in 2018, most observers thought we’d topped out what our approach could do. But we knew from our metrics that we were right on the edge of success and mostly needed more training. This meant the demands on my time had relented, and in November 2018, I felt I had an opening to take a gamble with three months of my time. Team members in high spirits after losing our first game at The International. I learn best when I have something specific in mind to build. I decided to try building a chatbot. I started self-studying the curriculum we developed for our Fellows program, selecting only the NLP-relevant modules. For example, I wrote and trained an LSTM language model and then a Transformer-based one. I also read up on topics like information theory and read many papers, poring over each line until I fully absorbed it. It was slow going, but this time I expected it. I didn’t experience flow state. I was reminded of how I’d felt when I just started programming, and I kept thinking of how many years it had taken to achieve a feeling of mastery. I honestly wasn’t confident that I would ever become good at machine learning. But I kept pushing because… well, honestly because I didn’t want to be constrained to only understanding one part of my projects. I wanted to see the whole picture clearly. My personal life was also an important factor in keeping me going. I’d begun a relationship with someone who made me feel it was ok if I failed. I spent our first holiday season together beating my head against the machine learning wall, but she was there with me no matter how many planned activities it meant skipping. One important conceptual step was overcoming a barrier I’d been too timid to do with Dota: make substantive changes to someone else’s machine learning code. I fine-tuned GPT-1 on chat datasets I’d found, and made a small change to add my own naive sampling code. But it became so painfully slow as I tried to generate longer messages that my frustration overwhelmed my fear, and I implemented GPU caching — a change which touched the entire model. I had to try a few times, throwing out my changes as they exceeded the complexity I could hold in my head. By the time I got it working a few days later, I realized I’d learned something that I would have previously thought impossible: I now understood how the whole model was put together, down to small stylistic details like how the codebase elegantly handles TensorFlow variable scopes. Retooled # After three months of self-study, I felt ready to work on an actual project. This was also the first point where I felt I could benefit from the many experts we have at OpenAI, and I was delighted when Jakub and my co-founder Ilya Sutskever agreed to advise me. Ilya singing karaoke at our company offsite. We started to get very exciting results, and Jakub and Szymon joined the project full-time. I feel proud every time I see a commit from them in the machine learning codebase I’d started. I’m starting to feel competent, though I haven’t yet achieved mastery. I’m seeing this reflected in the number of hours I can motivate myself to spend focused on doing machine learning work — I’m now around 75% of the number of coding hours from where I’ve been historically. But for the first time, I feel that I’m on trajectory. At first, I was overwhelmed by the seemingly endless stream of new machine learning concepts. Within the first six months, I realized that I could make progress without constantly learning entirely new primitives. I still need to get more experience with many skills, such as initializing a network or setting a learning rate schedule, but now the work feels incremental rather than potentially impossible. From our Fellows and Scholars programs, I’d known that software engineers with solid fundamentals in linear algebra and probability can become machine learning engineers with just a few months of self study. But somehow I’d convinced myself that I was the exception and couldn’t learn. But I was wrong — even embedded in the middle of OpenAI, I couldn’t make the transition because I was unwilling to become a beginner again. You’re probably not an exception either. If you’d like to become a deep learning practitioner, you can. You need to give yourself the space and time to fail. If you learn from enough failures, you’ll succeed — and it’ll probably take much less time than you expect. At some point, it does become important to surround yourself by existing experts. And that is one place where I’m incredibly lucky. If you’re a great software engineer who reaches that point, keep in mind there’s a way you can be surrounded by the same people as I am — apply to OpenAI!
The text of my speech introducing OpenAI Five at Saturday’s OpenAI Five Finals event, where our AI beat the world champions at Dota 2: “Welcome everyone. This is an exciting day. First, this is an historic moment: this will be the first time that an AI has even attempted to play the world champions in an esports game. OG is simply on another level relative to other teams we’ve played. So we don’t know what’s going to happen, but win or lose, these will be games to remember. And you know, OpenAI Five and DeepMind’s very impressive StarCraft bot This event is really about something bigger than who wins or loses: letting people connect with the strange, exotic, yet tangible intelligences produced by today’s rapidly progressing AI technology. We’re all used to computer programs which have been meticulously coded by a human programmer. Do one thing that the human didn’t anticipate, and the program will break. We think of our computers as unthinking machines which can’t innovate, can’t be creative, can’t truly understand. But to play Dota, you need to do all these things. So we needed to do something different. OpenAI Five is powered by deep reinforcement learning — meaning that we didn’t code in how to play Dota. We instead coded in the how to learn. Five tries out random actions, and learns from a reward or punishment. In its 10 months of training, its experienced 45,000 years of Dota gameplay against itself. The playstyle it has devised are its own — they are truly creative and dreamed up by our computer — and so from Five’s perspective, today’s games are going to its first encounter with an alien intelligence (no offense to OG!). The beauty of this technology is that our learning code doesn’t know it’s meant for Dota. That makes it general purpose with amazing potential to benefit our lives. Last year we used it to control a robotic hand that no one could program. And we expect to see similar technology in new interactive systems, from elderly care robots to creative assistants to other systems we can’t dream of yet. This is the final public event for OpenAI Five, but we expect to do other Dota projects in the future. I want to thank the incredible team at OpenAI, everyone who worked directly on this project or cheered us on. I want to thank those who have supported the project: Valve, dozens of test teams, today’s casters, and yes, even all the commenters on Reddit. And I want to give massive thanks today to our fantastic guests OG who have taken time out of their tournament schedule to be here today. I hope you enjoy the show — and just to keep things in perspective, no matter how surprising the AIs are to us, know that we’re even more surprising to them!”
This post is co-written by Greg Brockman (left) and Ilya Sutskever (right). We’ve been working on OpenAI for the past three years. Our mission is to ensure that artificial general intelligence (AGI) — which we define as automated systems that outperform humans at most economically valuable work — benefits all of humanity. Today we announced a new legal structure for OpenAI, called OpenAI LP, to better pursue this mission — in particular to raise more capital as we attempt to build safe AGI and distribute its benefits. In this post, we’d like to help others understand how we think about this mission. Why now? # The founding vision of the field of AI was “… to proceed on the basis of the conjecture that every aspect of learning or any other feature of intelligence can in principle be so precisely described that a machine can be made to simulate it”, and to eventually build a machine that thinks — that is, an AGI. But over the past 60 years, progress stalled multiple times and people started thinking of AI as a field that wouldn’t deliver. Since 2012, deep learning has generated sustained progress in many domains using a small simple set of tools, which have the following properties: Generality: deep learning tools are simple, yet they apply to many domains, such as vision, speech recognition, speech synthesis, text synthesis, image synthesis, translation, robotics, and game playing. Competence: today, the only way to get competitive results on most “AI-type problems” is through the use of deep learning techniques. Scalability: good old fashioned AI was able to produce exciting demos, but its techniques had difficulty scaling to harder problems. But in deep learning, more computational power and more data leads to better results. It has also proven easy (if costly) to rapidly increase the amount of compute productively used by deep learning experiments. The rapid progress of useful deep learning systems with these properties makes us feel that it’s reasonable to start taking AGI seriously — though it’s hard to know how far away it is. The impact of AGI # Just like a computer today, an AGI will be applicable to a wide variety of tasks — and just like computers in 1900 or the Internet in 1950, it’s hard to describe (or even predict) the kind of impact AGI will have. But to get a sense, imagine a computer system which can do the following activities with minimal human input: Make a scientific breakthrough at the level of the best scientists Productize that breakthrough and build a company, with a skill comparable to the best entrepreneurs Rapidly grow that company and manage it at large scale The upside of such a computer system is enormous — for an illustrative example, an AGI following the pattern above could produce amazing healthcare applications deployed at scale. Imagine a network of AGI-powered computerized doctors that accumulates a superhuman amount of clinical experience, allowing it to produce excellent diagnoses, deeply understand the nuanced effect of various treatments in lots of conditions, and greatly reduce the human error factor of healthcare — all for very low cost and accessible to everyone. Risks # We already live in a world with entities that surpass individual human abilities, which we call companies. If working on the right goals in the right way, companies can produce huge amounts of value and improve lives. But if not properly checked, they can also cause damage, like logging companies that cut down rain forests, cigarette companies that get children smoking, or scams like Ponzi schemes. We think of AGI as being like a hyper-effective company, with commensurate benefits and risks. We are concerned about AGI pursuing goals misspecified by its operator, malicious humans subverting a deployed AGI, or an out-of-control economy that grows without resulting in improvements to human lives. And because it’s hard to change powerful systems — just think about how hard it’s been to add security to the Internet — once they’ve been deployed, we think it’s important to address AGI’s safety and policy risks before it is created. OpenAI’s mission is to figure out how to get the benefits of AGI and mitigate the risks — and make sure those benefits accrue to all of humanity. The future is uncertain, and there are many ways in which our predictions could be incorrect. But if they turn out to be right, this mission will be critical. If you’d like to work on this mission, we’re hiring! About us # Ilya: I’ve been working on deep learning for 16 years. It was fun to witness deep learning transform from being a marginalized subfield of AI into one the most important family of scientific advances in recent history. As deep learning was getting more powerful, I realized that AGI might become a reality on a timescale relevant to my lifetime. And given AGI’s massive upside and significant risks, I want to maximize the positive parts of this impact and minimize the negative. Greg: Technology causes change, both positive and negative. AGI is the most extreme kind of technology that humans will ever create, with extreme upside and downside. I work on OpenAI because making AGI go well is the most important problem I can imagine contributing towards. Today I try to spend most of my time on technical work, and also work to spark better public discourse about AGI and related topics.
More in programming
It's time for a dev blogger's favorite topic: blogging about my new blogging setup. After migrating my blog from Hexo to Eleventy, building a photography website and creating a personal one-pager, it was time to find a new digital home for all my projects. If you notice any issue after the migration, please let me know. I redirect the RSS feed to the new domain, but you might want to update the feed URL in your reader. Motivation I've been maintaining three different websites under two different names: a mostly developer-oriented blog, a photography portfolio and a one-pager gluing everything together. When I started blogging about my sabbatical journey, I was thinking about making yet another page to create a clear separation. I've decided to do the opposite and merge all my websites into one instead. As I further reduce my dependency on social media and third-party services, it has become even more important to have my own space and practice POSSE. Every decision I've made was driven by two main goals: simplicity and personality. Setup I've stopped chasing the latest and greatest. I want to keep the setup as clean as possible, which means pulling fewer dependencies. Every package adds complexity and maintainability costs, so I've only added what was necessary — no more Gulp, Sass or Tailwind. Instead of taking one of my existing websites as a starting point, I created this project from scratch. I reused some of my previous code, but I didn't want to take over any technical debt (especially with the advancements in CSS). The foundation of this website hasn't changed: Eleventy (Build Awesome). It covers the fundamentals of a static site and offers a great level of customization. I've made my project more and less dependent on Eleventy at the same time: I replaced Gulp as my build pipeline with pure Eleventy processing. I replaced most shortcodes with Markdown alternatives (and markdown-it plugins), which are better supported by editors like Obsidian. I've also replaced the Nunjucks template engine with Liquid, which comes with a few trade-offs. The main reason was to avoid creating a new collection for every set of posts I render. With Liquid, you can apply array functions like filtering or sorting in the template itself: {% assign drafts = collections.all | where: "data.isDraft" | reverse %} {% for draft in drafts %} … {% endfor %} Another advantage is having date formatting included, so I don't need another library or custom code. You can even set a default date format, so you don't have to provide it each time. What I don't like is the verbosity of the template language, although I've gotten used to it by now. Nunjucks: {{ "/tags/" + tag + "/" }} {{ modified or created or date }} {% set perspectives = ["dev", "photography", "life"] %} Liquid: {{ "/tags/" | append: tag | append: "/" }} {{ modified | default: created | default: date }} {% assign perspectives = "dev, photography, life" | split: ", " %} After migrating my existing content, I've noticed the build taking way too long, both in the dev server and the production build. I've found out that the Eleventy template cache is turned off for Liquid by default. This was useful during development, but when updating actual content, it caused every template and partial to be constantly recalculated. After turning the cache on, this issue was resolved: eleventyConfig.setLiquidOptions({ cache: true }); Check the colophon for the full tech stack overview. Perspectives The main challenge was finding a way to separate and present my main personas. The Venn diagram intersection of people interested in software development, photography and my personal life is probably small. I could use tags, but they don't offer the level of personalization I was looking for. Most personal blogs separate the content by post type (blog post, note, photo), but this doesn't fit my use case. Instead, I've decided to structure my website around three perspectives, a term I have borrowed from the Eclipse IDE. Each perspective comes with its own avatar, tagline and bio. If someone is interested in everything I share (hi Mom!), they can still view all my posts. Design Since building my photography website, I've made many changes, but I never got around to share an update. The biggest change was the design, which I have used as a blueprint for the new website. I've decided to mimic a social timeline/feed, which most people are familiar with. At the same time, it should be clear that it's a personal website and not yet another walled garden. Typography is an important part of every design. The first step is to choose between a system font stack and a custom font, which comes with the usual performance vs. personality trade-offs. I've decided to go with a uniform look across all devices. I've settled on Source Sans 3 for copy text and Hack for code. Another big change was introducing a sidebar. I always felt like the empty horizontal space was "wasted" on a desktop device, but getting the sidebar UX right is challenging. I'm happy with the result, especially with the last two panels adding a personal touch to the design. Images I've made several changes to the handling of image files. To make them compatible with Markdown editors, I had to replace my custom picture shortcode. The official Eleventy image plugin is a popular choice, but it violates my simplicity goal and doesn't support everything I needed. I've decided to go with a plain Markdown image markup and a single image file (no format fallback or 2x version for retina displays). Additionally, I wrote a markdown-it plugin to turn the optional title parameter into a caption. As for the file format, both WebP and AVIF have excellent browser support. For each image, I select the format based on the quality and file size. For my photos, I'm using WebP for the timeline thumbnails and JPG for the full-size version. Theme picker There have been some recent discussions about dark mode toggles, and I considered going for a light/dark solution based on the user's OS preference. Driven by the personality goal, I knew I had to add at least one more theme, so a proper theme picker was necessary. Similar to navigation, people like to hide the theme selection on mobile devices behind a burger menu or a dropdown. I prefer the solution by Sarah L. Fossheim, where all available themes are displayed above the page footer. A friend was scrolling through my new website, and their first question was "Why is the theme picker in the footer?". This tells me the solution was a good idea — I don't think he would have noticed a typical theme button in the header. The implementation follows standard practices: Use the light or dark theme as default based on the user's OS or browser preference. Store a selected theme in the local storage and use it as the single source of truth. Prevent a flash of inaccurate color theme. Make the theme play nicely with bfcache. I might have slightly gone overboard with my third theme, but the nostalgia kept me adding more and more features. Enjoy going back to the 90s: Feeds As browser vendors decided to remove XSLT support, I didn't bother to style my feed. Instead, I've created a new web feeds page, listing all available feeds and a short explanation. I provide one feed for every perspective and one with all my posts combined. I once offered tag-based RSS feeds. This was useful for my sabbatical, but even there I had only a single subscriber. Now, all tag feeds redirect to their main perspective feed. I've also dropped the JSON feed, despite having at least 16 subscribers. Given that every web feed reader supports Atom feeds, I don't see the value of providing a JSON alternative. A JSON feed doesn't add much complexity, but handling different media types (post, photo) and properly escaping the content is one more thing to implement and maintain. Quality assurance I've put a lot of effort into my new digital home, and I want to make sure it stays in good shape. I've included several tools, most running automatically as part of the CI pipeline: eleventyDataSchema — lint Eleventy data values like titles or dates markdownlint — format/lint Markdown files Vale — lint content (grammar, writing style, formatting) Lychee — find broken links (internal and external) Evaluatory — check accessibility and validate HTML Oxlint — lint JavaScript files Eleventy linters are another option to analyze a template's output, but I haven't found a use case for it yet. Not everything can be automated, so I also did the following manually: Test with Chrome, Firefox and Safari Test with Polypane Test with Google Lighthouse Test with a screen reader Test with JavaScript disabled Test with CSS disabled Test at 200% zoom Test with a bigger browser font size Test with custom fonts disabled Test with delayed assets Performance It's difficult to compare the performance of two separate websites with that of a combined one. Some parts got worse, e.g. bigger font files and more CSS. At the same time, I've noticed that gzip compression was turned off for anything but HTML for a few years now (I must have missed it when upgrading nginx) 🙈. One improvement was to implement on-demand assets. For example, the lite-youtube-embed CSS and JavaScript is only loaded if a <lite-youtube> element is present on the page. As before, JavaScript is fully optional (except for interactive demos in individual posts). I've been using a mix of <noscript> and CSS to hide elements that require JS (like the theme picker). I don't like the user experience of pagination, so I avoided it for years. But now that I have almost 300 posts, I've decided to finally add it. I display 30 posts per page, with images further down still being lazy-loaded for better performance. I haven't applied pagination to tag pages, as that's more complex with Eleventy. Bits and pieces Here are some other noteworthy changes: I've removed post-specific social share images and use a single WebP Open Graph image instead. I've dropped JSON-LD. Open Graph and microformats are sufficient. "Naming is hard" is a common expression in software development, but it also applies to photograph titles. I've decided to use a counter as the title, e.g. #0100. Sometimes I do come up with a title that I love, like the Castle in the Sky, so I might mention it in the description. On a photo page, you can now open the previous/next photo using the left/right arrow keys (in addition to the existing navigation buttons). It's now possible to filter photos by date, gear, or location. Check the explore page for an overview. I've learned about and used some experimental CSS features (with progressive enhancement), like scroll-triggered animations or squircle corners. I've added an AI statement on my LLM usage. I finally have a proper about page. Cool URIs don't change, but it's inevitable when moving to a new domain. All my previous URLs are redirected, except for some assets that are no longer used. I will keep the old domain, so the redirects will continue to work for a foreseeable future. I've kept my previous Easter egg and added a few more. Have a happy hunt! 🕵️♂️ Inspiration I visited hundreds of personal websites looking for inspiration. Here are a few that I kept coming back to: Emma Goto. I love Emma's approach to presenting the variety of her interests, even though I went a different route. Brennan Kenneth Brown. This website has so much to offer. It's a great example of what the open web looks like. Rex Barrett. The sentence "Collecting hobbies is my hobby." hit home — so much that I had to steal borrow it for my short life bio. Conclusion This project was mostly about merging the content of two websites into one. I knew this would take a considerable amount of time, but I still greatly underestimated the effort. In the end, it was all about the journey, not the destination. I loved fine-tuning the design and tweaking little details that probably no one will ever notice. I still have many more ideas left in my backlog. How do you like my new digital home? If you have an opinion, let me know! Reply via email
New electronica album by Warrington-Runcorn New Town Development Plan
To protect against link rot and disappearing content, I save local copies of web pages I like into my web archive. Ideally, this archive is completely self-contained. I want a local copy of each page’s HTML and all the assets it depends on (CSS, images, scripts, and so on). I don’t want my archived pages fetching resources from the original website, which could disappear at any time. Initially, I was verifying self-containedness by hand. I’d open each saved page in a web browser, and check the browser dev tools. I’d check the Console tab for errors and failed requests, and watch the Network tab for requests going out to the internet. That approach was tedious and error-prone, and I wanted something better. I now automate the process using Playwright, a testing framework that allows you to control web browsers. I open each page using Playwright, and check if it’s making external requests. Detecting external requests in Playwright When you open a page in Playwright, you can subscribe to request events which tell you when the page issues a request. In this Python snippet, we start Playwright, start a WebKit browser, and open a new page. We tell the page to append external requests to a list, and then load example.com. We see that example.com makes two requests on page load: one for https://example.com, and one for a JavaScript file. from contextlib import closing from playwright.sync_api import sync_playwright with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, closing(browser.new_page()) as page, ): requests = [] page.on("request", lambda msg: requests.append(msg)) page.goto("https://example.com") print("requests:", requests) # requests: [<Request url='https://example.com/' method='GET'>, # <Request url='https://example.com/s.js' method='GET'>] In a more complex scenario, we might use Playwright to interact with the page after loading – for example, clicking a button or filling in a form, and then we could see the requests triggered by each action. Once we have a list of requests made by the page, we can inspect the url property to check if any of them were fetching from an external website. I wrote a function is_external_request to carry out this check: from playwright.sync_api import Request import urllib.parse def is_external_request(req: Request, original_url: str) -> bool: """ Check if a request has a different origin than the original URL. A request is considered internal if: - it matches the original URL exactly - it has the same scheme, host, and port as the original URL - it uses a `blob:` scheme (for example, in-memory media representations) """ if req.url == original_url: return False # Compare the scheme/host of the request to that of the original URL. org_url = urllib.parse.urlsplit(original_url) req_url = urllib.parse.urlsplit(req.url) if org_url.scheme == req_url.scheme and org_url.netloc == req_url.netloc: return False # Ignore blob: URLs, which are in-memory representation of some data, # and not an external request. # # Playwright detects blob: URLs when loading media files; for example # using the <audio> or <video> tag. if req_url.scheme == "blob": return False # Fail open: if we can't decide if a request was internal or external, # assume it was external. return True assert_page_is_self_contained(ctx, url) I’ve wrapped these two steps in a function assert_page_is_self_contained(ctx, url), which is part of my chives utility library. The function takes a Playwright Browser (or BrowserContext) and a URL, then: Opens a new page in the browser Loads the URL in the page Checks what happened when the page completes loading It throws an exception if there are any: Uncaught page exceptions or errors Console warnings or errors Non-GET requests Requests targeting external resources The function tries to trigger important network requests that might be skipped on the initial page load. For example, it evaluates the body’s offsetHeight to force a synchronous reflow, which causes the browser to request any lazily loaded images or fonts that might otherwise be missed. Here’s an example which verifies a file example.html from the working directory: from chives.browser_fixtures import assert_page_is_self_contained, file_uri from contextlib import closing from playwright.sync_api import sync_playwright with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, ): assert_page_is_self_contained(browser, url=file_uri("example.html")) I originally passed a Page as the first argument, but changing the function to take a Browser or BrowserContext lets it manage its own pages. It gets a fresh page for every verification, and closes it when it’s done. (The first time I ran this, I created a new page for every verification, and never closed them – my computer ground to a crawl as my script opened hundreds of browser tabs.) If you create a new context, you can emulate different browser settings, like location, locale, and window size. Here’s an example where we use contexts to test the same page in light/dark mode: with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, ): with closing(browser.new_context(color_scheme="light")) as light_ctx: assert_page_is_self_contained(light_ctx, url=file_uri("example.html")) with closing(browser.new_context(color_scheme="dark")) as dark_ctx: assert_page_is_self_contained(dark_ctx, url=file_uri("example.html")) In my web archive, I check every page in light/dark mode, and at 1×/2× pixel densities. It takes at least a second to run each verification, so I cache the results. As I save new pages, they’re added to a verification queue, and I’m warned of any missing assets. Unsurprisingly, the first run flagged many missing fonts and images, which I added to my local archive. Every saved page now passes cleanly. Browser is best-effort, but may still miss things Even with automated testing, it’s difficult to guarantee a completely self-contained archive. Different instances of a browser might load different assets, and a single page load might miss different variations: Responsive images – web pages can offer different sizes of an image, and the browser chooses a specific size based on the current window dimensions Appearance settings – a browser might load different fonts or images depending on light/dark mode Interaction-dependent assets – a web page might have assets that are only required on hover or user action You can mitigate some of this by testing with multiple contexts or adding post-load interactions, but every step makes the process take longer. Fortunately, most of the pages I save are simple, blog-style pages where I’ve stripped out the JavaScript. For my specific use case, initial load verification is more than good enough. Other uses for this code There are other projects where it could be useful to intercept and audit the requests made by a web page: Verifying your Content Security Policy (CSP). The CSP header restricts what resources can be loaded by a web page, and you could check that your configuration has the correct effect. Auditing login screens. You could check that sensitive pages don’t load any external scripts, which mitigates the risk of credential theft via a compromised third-party. Testing analytics and tracking. You could script user flows, like a shopping cart checkout, and confirm that tracking events fire correctly. I don’t do much front-end web development, but Playwright has become a key tool in my toolbox. I read a lot about people using AI agents and MCPs to interact with websites, but I prefer Playwright. It’s more fiddly and requires explicit instructions, but it’s constrained, deterministic, and I never have to worry about it doing something unexpected. [If the formatting of this post looks odd in your feed reader, visit the original article]
It’s four years since I bought a printer. I thought I would provide an update on running costs, print sales, and profit. I’m happy to be completely transparent about all this as I know it can be difficult to figure this out if you are thinking of creating your own printing business. January 11th 2026, 11:30am All the photos here are work shot in the last twelve months. Year Four Investment? Nothing other than consumables this year. I did purchase a high-spec film scanner last year, detailed here, but don’t consider that as part of this. The scanner has paid for itself several times over, and it may become a factor in future years (see below). Income Income is made up of sales through a local gallery that I am part of, along with web sales: 5,940.- CHF - sales through the gallery 80.- CHF - sales through web shop = 6,020.- CHF total income Total income is down about 50% compared to 2025. Costs I had to purchase consumables (paper and ink), at a total of 957.- CHF. That cost is included in the total here under the “print consumables” category. January 11th 2026, 3:30pm Rental cost of space in the gallery was reduced due to having overpayments of rent and my loan to the gallery, from the previous year(s), paid back to me. -1,500.- CHF - rental cost of space in the gallery -957.- CHF - print consumables (paper, ink) -1,138.- CHF - framing/mounting/postage -1,135.- CHF - commission to the gallery -300.- CHF - web shop subscription 0.- CHF - stripe / paypal fees 0.- CHF - advertising (none this year) = -5,030.- CHF total costs Total costs are down by about 60% compared to 2025. Total Profit 6,020.- CHF total income -5,030.- CHF total costs = 990.- CHF total profit A Quiet Year? Actually an average year. The previous couple of years were outliers, due to large orders, so really I’m back to normal in terms of sales. The key factors in managing to generate a [small] profit this year were: 1) no longer having to pay back the loan used to finance the printer, and 2) reduced rental cost of space in the gallery due to a previous loan to it being repaid. Also having to spend almost 1,000.- CHF on ink this year ate into profit. This is probably a “once every three years” cost given the rate I print at. The principal cost is rental of space in the gallery - in a normal year this would be 3,000.- CHF. Essentially this means, given the material cost of printing and framing/mounting and commission to the gallery, I need to make about 5,500.- CHF of sales to break even. Solar Eclipse, August 12th 2026 That’s a grind. However I did receive two substantial orders at the end of October, which haven’t made it in to this year’s figures and should boost next year’s. Maybe even a good sign for the year to come? At least now the original investment loan is repaid, the printing costs cover themselves in most years. I may look at offering scanning services now that I have a high-spec film scanner, which could become a scan + print service. More thoughts about that next time.