{"id":24,"date":"2026-10-27T14:00:00","date_gmt":"2026-10-27T08:00:00","guid":{"rendered":"https:\/\/nazmulrobin.com\/blog\/?p=24"},"modified":"2026-10-26T00:46:20","modified_gmt":"2026-10-25T18:46:20","slug":"contributing-to-wordpress-core-with-ai","status":"publish","type":"post","link":"https:\/\/nazmulrobin.com\/blog\/contributing-to-wordpress-core-with-ai\/","title":{"rendered":"How I contribute to WordPress core with AI"},"content":{"rendered":"\n<p>I contribute to WordPress core, and for the last while I have done it with an AI coding assistant, Claude Code, working next to me. This post is the workflow I settled on: how a ticket is picked, how the fix is checked, and what I do myself.<\/p>\n<p>It is not a way to produce patches in bulk. Core has reviewers with limited time, and a pull request that nobody tested costs them more than it gives. Everything below is built around sending a small number of changes that are correct.<\/p>\n<h2>The workflow in one list<\/h2>\n<ol>\n<li>Find an easy ticket that nobody has claimed.<\/li>\n<li>Reproduce the problem on a local copy of WordPress and measure it.<\/li>\n<li>Make the smallest change that fixes it.<\/li>\n<li>Verify the fix in a real browser, with the same measurement as before.<\/li>\n<li>I read the diff and test it by hand.<\/li>\n<li>Open the pull request, say that AI was used, and link it on the Trac ticket.<\/li>\n<\/ol>\n<p>The assistant does most of steps 1 to 4. Steps 5 and 6 are decisions, and those stay with me.<\/p>\n<h2>Writing the workflow down once<\/h2>\n<p>Claude Code has a feature called skills: a Markdown file of instructions that it loads when a task matches. I wrote one for core contributions. It holds my preferences, the local setup and the things learned from earlier attempts, so I do not explain them again in every session.<\/p>\n<p>These are the rules at the top of it:<\/p>\n<ul>\n<li><strong>Current WordPress only.<\/strong> No tickets for old default themes or legacy-only features.<\/li>\n<li><strong>Easy tickets, minimal changes.<\/strong> The smallest diff that fixes the ticket, with no extra fixes along the way. A related problem is mentioned as a possible follow-up and left out of the patch.<\/li>\n<li><strong>A short pull request description.<\/strong> One sentence on what changed, testing instructions, and what the template requires.<\/li>\n<li><strong>Ask before anything public.<\/strong> No commit, push or pull request without my go-ahead.<\/li>\n<\/ul>\n<p>The skill also keeps a history: which tickets were done, which were rejected and why. That list stops the same unsuitable ticket from being suggested twice.<\/p>\n<h2>Picking a ticket<\/h2>\n<p>This is the step where most of the time goes, and where I am strictest. I focus on accessibility tickets outside the editor.<\/p>\n<p>A ticket is rejected if any of these is true:<\/p>\n<ul>\n<li>It has an owner, or someone commented that they will open a pull request.<\/li>\n<li>A pull request is already linked, or it carries the <code>has-patch<\/code> keyword.<\/li>\n<li>It carries <code>needs-design<\/code>, <code>2nd-opinion<\/code>, <code>dev-feedback<\/code> or <code>close<\/code>.<\/li>\n<li>A committer disputes it or raises backward compatibility.<\/li>\n<li>It needs a discussion or a decision before any code.<\/li>\n<li>It is a tracking ticket for work that happens in Gutenberg.<\/li>\n<\/ul>\n<p>Two things I learned here. The <code>good-first-bug<\/code> keyword is not a reliable source, because those tickets almost always have a patch already. The best source is recently filed tickets from the accessibility team, which are often small, clear pieces split off from bigger work.<\/p>\n<p>Before any code is written, a search on GitHub confirms there is no open pull request for the ticket. Then the assistant shows me the pick, why it is easy and which files it touches, and I say yes or no. I say no often.<\/p>\n<h2>Reproduce first, then fix<\/h2>\n<p>The local setup is the standard <code>wordpress-develop<\/code> checkout with its Docker environment. Every ticket gets a fresh branch from current <code>trunk<\/code>.<\/p>\n<p>The rule that matters most is to measure before changing anything. For a focus style, that means loading the real admin screen in a browser with Playwright, moving focus to the element and reading its computed styles. The same script runs again after the fix, and the two results are compared. This shows that the bug existed, that the fix changed it, and that nothing else changed.<\/p>\n<p>For CSS in wp-admin, the check runs under several admin color schemes and not just the default one. A fix that looks right in one scheme can be wrong in another.<\/p>\n<p>Then the project&#8217;s own checks have to pass: the build, the linters and the relevant tests.<\/p>\n<h2>Two examples<\/h2>\n<p>Both of these are open for review as I write this.<\/p>\n<p><strong>A focus ring that disappeared.<\/strong> Buttons marked <code>aria-disabled=\"true\"<\/code> stay focusable, which is the point of that attribute. When such a button also had the <code>.disabled<\/code> class, a <code>box-shadow: none !important<\/code> rule removed its focus ring, so keyboard users could not see where they were. The fix is a small CSS change in one file. It is <a href=\"https:\/\/core.trac.wordpress.org\/ticket\/66271\">ticket #66271<\/a> and <a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/pull\/14158\">pull request #14158<\/a>.<\/p>\n<p><strong>Uploads where uploading makes no sense.<\/strong> In the media modal, dropping a file started an upload even in states such as cropping or editing an image, where there is nothing to upload to. This one is JavaScript and was harder to verify, because a drop made by a script does not behave like a real one. It is <a href=\"https:\/\/core.trac.wordpress.org\/ticket\/47130\">ticket #47130<\/a> and <a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/pull\/14159\">pull request #14159<\/a>.<\/p>\n<p>There is a third one that never became a pull request. A fix for pausing video headers was written and working when I dropped it, because the feature is legacy and I had decided not to spend reviewers&#8217; time there. That decision is now a rule in the skill.<\/p>\n<h2>Saying that AI was used<\/h2>\n<p>The core pull request template has a section for AI tools, and I fill it in every time:<\/p>\n<pre><code class=\"language-markdown hljs\"><span class=\"hljs-section\">## Use of AI Tools<\/span>\n\nAI assistance: Yes\nTool(s): Claude Code\nModel(s): Claude Opus 5.5\nUsed for: Drafting the CSS change and verifying it with Playwright; reviewed and tested by me.<\/code><\/pre>\n<p>The last part is the important one. My name is on the pull request, so I have read every line and tested it myself. If a reviewer asks why the change is written that way, I can answer.<\/p>\n<p>Commits carry no AI co-author line. I am the author, and the disclosure belongs in the pull request, where the project asks for it.<\/p>\n<h2>What I keep for myself<\/h2>\n<ul>\n<li><strong>Choosing the ticket.<\/strong> The assistant proposes, and I decide whether it is worth a reviewer&#8217;s time.<\/li>\n<li><strong>Reading the diff.<\/strong> Every line, before it is committed.<\/li>\n<li><strong>Testing by hand.<\/strong> The automated check is evidence. I still open the screen and try it.<\/li>\n<li><strong>Everything public.<\/strong> Commits, pushes, the pull request and the comment on Trac.<\/li>\n<li><strong>The discussion.<\/strong> Review feedback is a conversation between people, and I take part in it myself.<\/li>\n<\/ul>\n<h2>What I would tell another contributor<\/h2>\n<ul>\n<li><strong>Start with the rejection rules.<\/strong> An assistant will happily fix a ticket that someone else is already working on. Tell it what to skip.<\/li>\n<li><strong>Demand a before and after.<\/strong> A fix without a measurement is a guess.<\/li>\n<li><strong>Keep the diff small.<\/strong> Small changes get reviewed, and large ones wait.<\/li>\n<li><strong>Write your rules down.<\/strong> Each correction I made became a line in the skill, and the next session started from there.<\/li>\n<li><strong>Disclose it.<\/strong> The project asks for it, and it costs nothing.<\/li>\n<\/ul>\n<p>AI did not change what a good core contribution is. It is still a clear ticket, a small change and proof that it works. What changed is how long the searching, setting up and measuring take.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I use Claude Code to help with WordPress core contributions. This is the workflow, the rules I gave it, and the parts I keep for myself.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[22,21,20],"class_list":["post-24","post","type-post","status-publish","format-standard","hentry","category-wordpress","tag-accessibility","tag-ai","tag-core-contribution"],"_links":{"self":[{"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/posts\/24","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/comments?post=24"}],"version-history":[{"count":2,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/posts\/24\/revisions"}],"predecessor-version":[{"id":109,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/posts\/24\/revisions\/109"}],"wp:attachment":[{"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/media?parent=24"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/categories?post=24"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/tags?post=24"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}