<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[CodeX - Medium]]></title>
        <description><![CDATA[Everything connected with Tech &amp; Code. Follow to join our 1M+ monthly readers - Medium]]></description>
        <link>https://medium.com/codex?source=rss----29038077e4c6---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>CodeX - Medium</title>
            <link>https://medium.com/codex?source=rss----29038077e4c6---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 06:01:01 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/codex" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[My Homelab Updated Itself Every Night. Except It Didn’t.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/my-homelab-updated-itself-every-night-except-it-didnt-8f5cd04d8348?source=rss----29038077e4c6---4"><img src="https://cdn-images-1.medium.com/max/1344/0*cRVHnZ_WpbUP2qX9.png" width="1344"></a></p><p class="medium-feed-snippet">How I set up an old Intel Mac mini as a web server behind Cloudflare, deploying web apps from GitHub onto Docker and running Ansible every&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/codex/my-homelab-updated-itself-every-night-except-it-didnt-8f5cd04d8348?source=rss----29038077e4c6---4">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/my-homelab-updated-itself-every-night-except-it-didnt-8f5cd04d8348?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/8f5cd04d8348</guid>
            <dc:creator><![CDATA[Brian Jones]]></dc:creator>
            <pubDate>Wed, 11 Nov 2026 20:46:01 GMT</pubDate>
            <atom:updated>2026-11-11T20:46:01.643Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Microsoft Fabric: How to Prove the Business Case Before You Scale]]></title>
            <link>https://medium.com/codex/microsoft-fabric-how-to-prove-the-business-case-before-you-scale-a8752fa7c0a2?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/a8752fa7c0a2</guid>
            <category><![CDATA[data-analytics]]></category>
            <category><![CDATA[business-intelligence]]></category>
            <category><![CDATA[microsoft-fabric]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[data-strategy]]></category>
            <dc:creator><![CDATA[Audacia]]></dc:creator>
            <pubDate>Wed, 11 Nov 2026 18:46:01 GMT</pubDate>
            <atom:updated>2026-11-11T18:46:01.694Z</atom:updated>
            <content:encoded><![CDATA[<p>By Oliver Boult, Lead Data Consultant</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/744/0*q8gZtStLHqXfr_ws.png" /></figure><p>Interest in Microsoft Fabric has grown quickly among organisations with complex data estates. The proposition is a single platform for data engineering, warehousing, data science, real-time intelligence and reporting, built on one storage layer and one model of governance. For organisations that have accumulated separate tools, teams and copies of the same data over many years, the prospect of consolidation is compelling.</p><p>A demonstration establishes that the platform can do what it promises under controlled conditions. The decision to extend it across an organisation requires evidence of a different kind — what the platform will cost at production volumes, whether the organisation’s data is ready to move, whether governance will hold as more teams arrive, and whether people will change the way they work.</p><p>This matters because the ability to evidence value remains a persistent weakness in data investment. In Gartner’s <a href="https://www.gartner.com/en/newsroom/press-releases/2025-02-20-gartner-survey-finds-one-third-of-cdaos-cite-measuring-data-analytics-and-ai-impact-as-top-challenge">most recent survey</a> of chief data and analytics officers, 30% named the inability to measure the impact of data, analytics and AI on business outcomes as their top challenge, and only 22% of organisations had defined, tracked and communicated business impact metrics for most of their use cases. A platform decision of Fabric’s scale deserves a stronger foundation. In our work with organisations evaluating Fabric, we have found that the most reliable foundation is a pilot designed from the outset as a decision instrument, tested against a meaningful business problem, measured from the first day, and concluded with an explicit choice to scale, refine or stop.</p><p>Our <a href="https://audacia.co.uk/blog/fabric-iq-and-ontology">earlier article</a> explored what Fabric consolidates and why that consolidation matters. This article considers how organisations can establish whether it will deliver value for them.</p><h3>Starting with the business problem</h3><p>The credibility of a pilot depends largely on the problem it is asked to solve. McKinsey has <a href="https://www.mckinsey.com/capabilities/tech-and-ai/our-insights/charting-a-path-to-the-data-and-ai-driven-enterprise-of-2030">described</a> how organisations often fall into what it calls “pilot purgatory”, with teams launching proofs of concept that have no chance of scaling, and stakeholders pursuing varied use cases that require an entire architecture to be built before any value can be realised. A Fabric pilot built around a technical showcase, such as migrating a sample dataset, risks the same outcome. It produces results that are interesting to technical teams and difficult to translate into a case a finance director can weigh.</p><p>The strongest pilots begin with an operational problem the business already recognises. In our experience, good candidates share a number of characteristics. They have a named business owner who feels the problem today and will judge the result. They carry a cost or value that can be expressed in the organisation’s own terms, whether hours of manual effort, delayed decisions, regulatory exposure or lost revenue. They involve data at representative volume and complexity, drawn from the systems the organisation runs. They also cross team boundaries, because much of Fabric’s benefit lies in bringing engineers, analysts and business users into a shared environment, and a pilot confined to a single team cannot test that effectively.</p><p>Scope and duration deserve equal consideration. A pilot needs to run long enough to produce a representative picture of how the platform will be used and consumed, and short enough to reach a decision while the business question remains live. For most organisations, a period of two to four months strikes that balance.</p><p>Month-end financial reporting offers a useful illustration to move from. In many large organisations, the close still depends on extracts from several source systems, manual reconciliation in spreadsheets and a small team working long hours at the start of each month. The problem is visible to senior leadership, its cost can be measured in time and error rates, and solving it requires data engineering, modelling and reporting to work together. It is the kind of problem that reveals whether a platform can deliver in a live environment.</p><h3>Six measures of a credible pilot</h3><p>A pilot becomes a decision instrument when it is judged against measures agreed before any work begins. We recommend six, each with a baseline taken at the outset and a threshold that would justify scaling. Agreeing both in advance gives the business owner, the technology team and the executive sponsor a shared definition of success, and it protects the eventual decision from being shaped by enthusiasm or by hindsight.</p><p><strong>Business outcomes:</strong> They should be expressed in the language of the business owner, such as the number of days taken to close the month or the time between a question being asked and a decision being made. Technical measures such as query performance matter to the extent that they move these outcomes.</p><p><strong>Data quality:</strong> This establishes whether consolidation improves the reliability of the information people use. This measure has gained weight as organisations look to build AI capabilities on their data platforms. Gartner <a href="https://www.gartner.com/en/newsroom/press-releases/2025-02-26-lack-of-ai-ready-data-puts-ai-projects-at-risk">predicts</a> that through 2026, organisations will abandon 60% of AI projects that are not supported by AI-ready data, and McKinsey <a href="https://www.mckinsey.com/capabilities/mckinsey-technology/our-insights/building-the-foundations-for-agentic-ai-at-scale">reports</a> that eight in ten companies cite data limitations as a barrier to scaling agentic AI. A pilot that demonstrates measurable improvement in data quality strengthens the business case well beyond reporting.</p><p><strong>Governance:</strong> This considers whether the platform can be controlled as effectively as it can be used. The pilot should test access control, lineage, sensitivity labelling and data loss prevention against the organisation’s own policies and regulatory obligations. The more important question concerns what happens as adoption grows, since controls that work well for a single team can weaken once many teams and workspaces are involved.</p><p><strong>Adoption:</strong> This reflects whether people change the way they work. Indicators include the number of active users, the reuse of shared datasets and models, and the retirement of spreadsheets and duplicate reports that the platform was intended to replace. Reuse deserves particular attention. McKinsey’s <a href="https://www.mckinsey.com/capabilities/mckinsey-digital/our-insights/the-missing-data-link-five-practical-lessons-to-scale-your-data-products">research</a> on data products found that reusable data assets can accelerate the capture of a use case’s value by as much as 90 percent, which makes early evidence of reuse one of the strongest predictors of value at scale.</p><p><strong>Capacity:</strong> This describes how the pilot consumes the platform’s shared compute resources. All Fabric workloads draw on a common pool of capacity, so the pattern of consumption observed during the pilot is the most reliable guide to sizing a production environment. Microsoft <a href="https://learn.microsoft.com/en-us/fabric/enterprise/optimize-capacity">provides</a> dedicated tooling for monitoring utilisation and identifying the operations that use the most compute, and we advise putting that monitoring in place on the first day, so the evidence accumulates across the full pilot.</p><p><strong>Cost:</strong> This draws the other measures together into a projected cost of running the platform at scale. It should account for capacity, storage growth, licensing and the effort required to operate and support the platform. The timing of commercial commitments is an important consideration here. Longer-term capacity commitments can reduce unit costs, and they are best made once the pilot has produced a dependable picture of consumption.</p><p>For the month-end reporting example, a pilot scorecard might take the following form:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/723/1*AxPenmT1xLP6G73AGo_lxA.png" /></figure><p>A pilot should conclude with one of three decisions, and each represents a valid outcome.</p><p><strong>Scale:</strong></p><p>A decision to scale is appropriate when the pilot has met its thresholds for business outcomes, data quality and cost, and when governance and adoption have held up under realistic conditions. It should be accompanied by a clear view of which use cases follow, the capacity they will require and the operating model needed to support them.</p><p><strong>Refine:</strong></p><p>A decision to refine is appropriate when the pilot has demonstrated real value while revealing weaknesses that would grow at scale. Strong business outcomes accompanied by limited adoption are a common example, as is a promising result offset by a consumption profile that would be costly to sustain. Refinement means addressing those weaknesses in a focused second phase before committing further, whether through optimisation, a stronger governance model or investment in skills.</p><p><strong>Stop:</strong></p><p>A decision to stop is appropriate when the evidence shows that the platform will not deliver sufficient value for the organisation at this time. Existing platforms may already meet the need, the data estate may require foundational work before any platform can succeed, or the projected cost may exceed the benefit. We regard a well-evidenced decision to stop as a successful pilot. It protects the organisation from a far larger commitment and leaves a clear record of the conditions under which the question could be revisited.</p><h3>Preparing for scale</h3><p>A successful pilot establishes the business case. Preparing the organisation to realise that case at scale requires further groundwork, and in our experience the organisations that scale most smoothly address four areas before the rollout widens.</p><p>The first is capacity planning, informed directly by the consumption evidence gathered during the pilot and structured to separate development and production workloads where this is justified. The second is the design of workspaces and data domains, which should reflect how the organisation is structured, so that ownership of data is clear and responsibility for governance can be delegated with confidence. The third is a formal governance model covering how workspaces are created, how datasets are certified and how sensitive information is protected. The fourth is a skills and adoption plan that addresses the full range of users, from engineers adopting new tooling to business teams moving from spreadsheets to shared models.</p><p>Sequencing is a further important consideration. Organisations that select their next wave of use cases using the same six measures, prioritising business value and data readiness, tend to build credibility with each phase. Those that attempt to migrate their entire estate at once often find that effort is spread too thinly for any single area to demonstrate clear value.</p><h3>Evidence before commitment</h3><p>Microsoft Fabric has the potential to simplify data estates that have become complex over many years, and to bring engineering, analytics and business teams into a shared way of working. Whether that potential is realised depends on each organisation’s data, governance, people and budget, as much as on the capabilities of the platform itself.</p><p>Audacia’s approach to Fabric evaluation follows the sequence we apply across our consultancy work. We begin by understanding the business problem, the current data estate and the outcomes that matter to the organisation. We then define the pilot, its scope and its measures with the business owner, recommending a decision based on the evidence the pilot produces, whether that means scaling, refining the approach or pausing until the conditions are right. The organisations that gain most from Fabric are those that treat the first pilot as a test of the business case as well as the technology, and that are prepared to act on whatever the evidence shows.</p><p><em>Originally published at </em><a href="https://audacia.co.uk/blog/microsoft-fabric-how-to-prove-the-business-case-before-you-scale"><em>https://audacia.co.uk</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a8752fa7c0a2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/microsoft-fabric-how-to-prove-the-business-case-before-you-scale-a8752fa7c0a2">Microsoft Fabric: How to Prove the Business Case Before You Scale</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Preservation Contracts: Safer AI Agent Changes in Legacy Code]]></title>
            <link>https://medium.com/codex/preservation-contracts-safer-ai-agent-changes-in-legacy-code-ae436324868f?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/ae436324868f</guid>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[legacy-code]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Oren Grinker]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 19:51:01 GMT</pubDate>
            <atom:updated>2026-11-10T19:51:01.709Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q4DnXfoeYWOz_KKJL6LuZQ.png" /></figure><p><em>Green tests don’t tell an AI agent what it must leave alone. A one-page preservation contract does.</em></p><p>Here is a failure many teams will meet this year. An agent fixes a bug in a records service. On the way, it “simplifies” a shared helper and removes a filter that looks redundant. Every test passes.</p><p>A week later, the CSV export shows users rows they were never meant to see. The export called the same helper, and no test checked whose rows ended up in the file.</p><p>The agent did exactly what it was asked. Nobody had written down what it must not change. That gap, not the agent, is what this article is about.</p><h3>Every change has two intents</h3><p>Every change to existing code carries two intents: what should change, and everything that must stay the same. Tickets capture the first. The second stays implicit, because to the people who know the system it feels obvious.</p><p>It is not obvious to an agent. The agent doesn’t know which teams call the helper, which incident created that odd-looking filter, or which customer depends on the sort order. It improves the code in front of it.</p><p>Tests don’t close the gap on their own. They catch only what someone once thought to check, and in old systems much of the behavior people rely on is checked by nobody.</p><p>Bug-fix specs give a clean way to state both intents. When the bug condition C holds, the fix must make the expected result P true. When C does not hold, the code must behave exactly as before:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/702/1*zbVifPqDdSFCzM1-l_h2bQ.png" /></figure><p>The second half is the one we skip. The rest of this article is about writing it down.</p><h3>Hidden contracts are everywhere</h3><p>With enough users, every observable behavior of a system becomes something somebody depends on, whatever the documentation promises. Engineers know this as <a href="https://www.unified-streaming.com/blog/misunderstanding-tech-ep-1-hyrums-law">Hyrum’s Law</a>.</p><p>In practice, hidden contracts look boring:</p><ul><li>the order of results</li><li>an empty list versus null (or None)</li><li>rounding rules</li><li>date and time formats</li><li>error and status codes</li><li>which rows a user is allowed to see</li></ul><p>The cheapest way to find them is to look at who calls the code you are about to change. One search for a shared helper might turn up this:</p><pre>grep -rl &quot;fetch_records(&quot; src/</pre><p>Caller</p><p>Relies on</p><p>Covered by a test</p><p>src/export/csv.py</p><p>only rows the user may see</p><p>No</p><p>src/mobile/feed.py</p><p>newest first</p><p>No</p><p>src/billing/run.py</p><p>an empty list, never None</p><p>Yes</p><p>src/api/partner.py</p><p>ISO 8601 dates</p><p>No</p><p>src/reports/tax.py</p><p>amounts rounded half-up</p><p>Yes</p><p>src/web/status.py</p><p>404 for a missing record, never 500</p><p>No</p><p>One function, six hidden contracts, four with no test. The example is illustrative, but the shape is common.</p><p>Treat an empty search result as limited knowledge, not proof. Reflection, configuration, scheduled jobs and external API clients rarely show up in a grep.</p><h3>The preservation contract</h3><p>A preservation contract is one page, written before the agent starts, with six fields. Each field closes a different shortcut an agent can take to “pass” a task.</p><p>Field</p><p>What it says</p><p>The shortcut it blocks</p><p>Condition</p><p>When the change applies</p><p>Quietly widening the problem</p><p>Expected result</p><p>What must be true afterwards</p><p>Hiding the error instead of fixing it</p><p>Protected behaviors</p><p>Guarantees this change must not break</p><p>“Cleaning up” a neighbor</p><p>Allowed scope</p><p>Files and modules it may touch</p><p>Silent changes elsewhere; anything outside is reported</p><p>Fix proof</p><p>A test that fails on the old code and passes after</p><p>Claiming a fix nobody can see</p><p>No-regression proof</p><p>Tests that pass both before and after</p><p>Breaking what nobody checked</p><p>For the helper above, a filled-in contract fits on one screen:</p><pre># change-contract.md: duplicate rows in fetch_records</pre><pre>Condition:        merged accounts return each record twice<br>Expected result:  every record is returned once<br>                  (not &quot;the error is gone&quot;)<br>Protected:<br>  - export returns only rows the user may see   owner: Data<br>  - feed order stays newest first                owner: Mobile<br>  - empty result is [], never None               owner: Billing<br>Allowed scope:    records/ only. Anything else is reported, not changed.<br>Fix proof:        test_no_duplicate_rows   fails on base, passes after<br>No-regression:    6 contract tests         pass on base, pass after</pre><p>The contract doesn’t replace tests, CI or review. It gives the reviewer something concrete to hold the diff against.</p><h3>Test the old code first</h3><p>The order of work matters more than the template: protective tests are written and run against the old code, before anyone touches the fix.</p><p>Tests written after the fix describe the new code. When the agent writes both the change and its tests, a green suite only proves that the code agrees with itself.</p><ol><li>Check out the base commit.</li><li>Write the fix test and confirm it fails.</li><li>Write the protective tests and confirm they pass.</li><li>Commit the tests on their own, before any change to the code.</li><li>Let the agent make the change, then run everything.</li></ol><p>Michael Feathers calls tests like these characterization tests: they record what the code does today, not what it should do. Keep them out of the agent’s reach while it works, so it can’t “fix” a test instead of the code.</p><p>After the change, the results should read like this:</p><p>Test</p><p>Old code</p><p>After the agent’s fix</p><p>no_duplicate_rows (fix proof)</p><p>Fail</p><p>Pass</p><p>export_visible_rows_only</p><p>Pass</p><p><strong>Fail</strong></p><p>feed_newest_first</p><p>Pass</p><p>Pass</p><p>billing_empty_list_not_none</p><p>Pass</p><p>Pass</p><p>Two outcomes are good: the fix test turns green, and every protective test stays green. A protective test that changes color means the change left its scope, so narrow it and run again.</p><p>In this run, the export test caught the same removed filter that slipped through in the opening story.</p><h3>Commitment or accident?</h3><p>A characterization test records a bug as faithfully as a feature. So every recorded behavior needs one more decision: is it a commitment someone relies on, or an accident that is free to change?</p><p>Excel shows how a bug becomes a commitment. It still treats 1900 as a leap year because Lotus 1–2–3 did, and <a href="https://support.microsoft.com/kb/214326">Microsoft explains</a> that a fix would shift almost every existing date by one day and break formulas.</p><p>G. K. Chesterton’s fence makes the same point: before you remove a fence, find out why it was put up. The contract is where that answer gets written down, before the agent removes the fence.</p><p>A named person makes each call, not the agent and not “the team”:</p><p>Recorded behavior</p><p>Decision</p><p>Who decides</p><p>Next step</p><p>Export shows only visible rows</p><p>Commitment</p><p>Data team lead</p><p>Protective test</p><p>Feed is sorted newest first</p><p>Not decided yet</p><p>Mobile product owner</p><p>Keep the current order until decided</p><p>Empty result is [], never None</p><p>Accident, after checking every caller</p><p>API owner</p><p>Test the new behavior and note it in the spec</p><p>The agent gets no permission to change a behavior that hasn’t been through this table.</p><h3>Grow the spec where the code changes</h3><p>You don’t need to document the whole system before the first change. Each contract adds a small delta to a living spec, and the spec grows exactly where the system moves.</p><p>A delta records three things: what this change added, what it modified, and what it removed. It also records the guarantees this change wrote down for the first time, such as “the export returns only rows the user may see.”</p><p>That second part matters. On day one a legacy spec is empty, so nothing is protected by it yet. Each approved guarantee moves from the contract into the spec, tied to a test that keeps guarding it.</p><p>After a few months, the spec is dense around the hotspots and thin where nothing changes. A full inventory still makes sense when it is the deliverable itself, such as a platform migration or an audit.</p><h3>Start with three changes</h3><p>Try the contract on three changes of growing difficulty, then decide whether to keep it. Each one teaches something different:</p><ol><li><strong>A small bug fix.</strong> The team learns the order: boundary first, protective tests on the old code, then the fix.</li><li><strong>A feature next to a busy file.</strong> The contract lists real callers and past incidents, or says plainly that there is no data.</li><li><strong>A refactoring.</strong> The commitment-or-accident table gets filled in for real, with a name on every row.</li></ol><p>Three signals say it is worth spreading: a protective test changed color and narrowed a patch, reviewers quote contract fields instead of impressions, and deltas reach the spec without extra work.</p><p>Three signals say stop or simplify: contracts appear for typo fixes, protected behaviors are copied from a template instead of found, or the contract repeats what review already catches. Patch size is not the test, though. A one-line change to authorization or payments may still need the full page.</p><h3>The takeaway</h3><p>In legacy code, specify the change, not the system. Before the first prompt, write down what must not change, prove it on the old code, and let a named person decide what counts as a commitment.</p><p>Your next ticket in old code can get a change-contract.md before the agent writes a single line. One page is enough to make a green build mean something.</p><p>Which behavior in your system turned out to be a commitment only after someone changed it?</p><p><em>This article builds on Jaroslaw Wasowski’s piece “Spec-Driven Development — Brownfield Spec and Preservation Contract” ([link to the original article]), which develops the preservation contract in more depth.</em></p><h3>Sources</h3><ul><li><a href="https://www.unified-streaming.com/blog/misunderstanding-tech-ep-1-hyrums-law">Hyrum’s Law, explained by Unified Streaming</a></li><li><a href="https://support.microsoft.com/kb/214326">Microsoft: Excel incorrectly assumes that the year 1900 is a leap year</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ae436324868f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/preservation-contracts-safer-ai-agent-changes-in-legacy-code-ae436324868f">Preservation Contracts: Safer AI Agent Changes in Legacy Code</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using MCP to Control a Raspberry Pi from Claude Desktop]]></title>
            <link>https://medium.com/codex/using-mcp-to-control-a-raspberry-pi-from-claude-desktop-1f25a01d469e?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/1f25a01d469e</guid>
            <category><![CDATA[raspberry-pi]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[mcp-server]]></category>
            <category><![CDATA[claude]]></category>
            <dc:creator><![CDATA[Francesco Azzola]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 18:56:01 GMT</pubDate>
            <atom:updated>2026-11-10T18:56:01.666Z</atom:updated>
            <content:encoded><![CDATA[<p>My goal was to connect Claude Desktop to a Raspberry Pi so it could read sensor data, reason about the physical environment, and trigger actions on the device through MCP.</p><p>For example:</p><ul><li>read temperature, humidity, or pressure</li><li>understand the current physical context</li><li>answer the user</li><li>optionally change the robot expression to happy, curious, or surprised</li></ul><p>The important architectural constraint was that Claude should not access sensor drivers, display code, or robot internals directly. That is where MCP becomes useful.</p><p>This diagram shows what i wanted to obtain:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ppfHqiVnKnASeHoKKDYTMA.png" /></figure><p>Phos is the name of my simple robot developed using Python on Raspberry Pi.</p><h3>MCP in one minute</h3><p>The Model Context Protocol provides a standard way for an AI client to discover and invoke tools exposed by an external system. For this use case, only two transport models matter.</p><h3>stdio</h3><p>A local MCP client starts a process and communicates with it through:</p><pre>stdin / stdout</pre><p>This model is convenient for desktop tools because the MCP server looks like a local executable.</p><h3>Streamable HTTP</h3><p>The MCP server runs independently and exposes an HTTP endpoint. Conceptually:</p><pre>AI client<br>    ↓<br>MCP over HTTP<br>    ↓<br>remote service</pre><p>This model fits a Raspberry Pi much better. The Pi is a continuously running physical device. Its lifecycle should not depend on whether Claude Desktop is connected. That leads to the architecture used here:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9T79NsXjECPNtLzdG-t41Q.png" /></figure><p>The Raspberry Pi exposes MCP over HTTP. Claude Desktop gets the stdio interface it expects. The bridge connects the two.</p><h3>Why use a bridge?</h3><p>A first implementation could make Claude Desktop connect to a process running directly on the Raspberry Pi over stdio. That works, but it creates an undesirable lifecycle dependency:</p><pre>Claude starts MCP<br>      ↓<br>MCP starts robot runtime</pre><p>For a physical device, I prefer the opposite model:</p><pre>Raspberry Pi starts<br>      ↓<br>robot runtime starts<br>      ↓<br>MCP endpoint becomes available<br>      ↓<br>Claude may connect later</pre><p>The robot continues operating when Claude disconnects. The local bridge solves the compatibility problem without changing that architecture. Its only responsibility is:</p><pre>MCP stdio<br>→<br>MCP Streamable HTTP</pre><p>It should not contain robot logic. It should not know how sensors work. It should not implement copies of Raspberry Pi functions. It simply forwards MCP operations between Claude Desktop and the remote MCP server.</p><p>A practical setup looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DosJ5UqDB_UCrHNUD-blnw.png" /></figure><p>The bridge can run on macOS, Linux, or Windows while the Raspberry Pi remains dedicated to the physical runtime.</p><h3>The Raspberry Pi should expose semantics, not hardware</h3><p>This is the most important part of the design. An MCP tool should not look like this:</p><pre>read_bme280_register<br>set_gpio_pin<br>draw_pixel<br>write_led_buffer</pre><p>Those are implementation details. Claude should see operations closer to the meaning of the physical system. For example:</p><pre>get_environment<br>get_robot_status<br>get_physical_context<br>set_expression<br>speak<br>sleep<br>wake</pre><p>The interaction path becomes:</p><pre>Claude Desktop<br>      ↓<br>MCP<br>      ↓<br>semantic application layer<br>      ↓<br>runtime services<br>      ↓<br>hardware</pre><p>Not:</p><pre>Claude Desktop<br>      ↓<br>MCP<br>      ↓<br>sensor driver</pre><p>This keeps the agent independent from the hardware implementation. A sensor can change later without changing the MCP contract. The same applies to the display. Claude does not need to understand how the eyes are rendered. It only needs a semantic operation such as:</p><pre>set_expression(&quot;curious&quot;)</pre><p>The Raspberry Pi decides how that expression is actually displayed.</p><h3>Building a semantic physical context</h3><p>A useful MCP tool can expose a compact description of what the Raspberry Pi currently perceives. For example:</p><pre>{<br>  &quot;environment&quot;: {<br>    &quot;temperature_c&quot;: 22.8,<br>    &quot;humidity_percent&quot;: 48,<br>    &quot;pressure_hpa&quot;: 1015<br>  },<br>  &quot;air_quality&quot;: {<br>    &quot;available&quot;: true,<br>    &quot;condition&quot;: &quot;good&quot;<br>  },<br>  &quot;presence&quot;: {<br>    &quot;detected&quot;: true<br>  },<br>  &quot;robot_state&quot;: &quot;idle&quot;<br>}</pre><p>The exact structure depends on the application. The important property is that this model should describe:</p><blockquote><em>what the physical system currently perceives</em></blockquote><p>rather than:</p><blockquote><em>which driver produced each value</em></blockquote><p>This layer can also include interpreted values. For example:</p><pre>{<br>  &quot;temperature_c&quot;: 28.4,<br>  &quot;temperature_condition&quot;: &quot;warm&quot;<br>}</pre><p>That allows the agent to reason over a stable semantic model instead of repeatedly interpreting low-level device data.</p><h3>Example: “How is the room?”</h3><p>Consider a simple interaction. The user asks Claude Desktop:</p><blockquote><em>How is the room?</em></blockquote><p>Claude does not need direct access to the Raspberry Pi. Instead, it calls an MCP tool such as:</p><pre>get_physical_context</pre><p>The request travels through:</p><pre>Claude Desktop<br>      ↓<br>stdio<br>      ↓<br>MCP bridge<br>      ↓<br>Streamable HTTP<br>      ↓<br>Raspberry Pi MCP server<br>      ↓<br>semantic application service<br>      ↓<br>sensor services</pre><p>The Raspberry Pi might return:</p><pre>{<br>  &quot;environment&quot;: {<br>    &quot;temperature_c&quot;: 27.1,<br>    &quot;humidity_percent&quot;: 61,<br>    &quot;pressure_hpa&quot;: 1008,<br>    &quot;condition&quot;: &quot;warm&quot;<br>  },<br>  &quot;air_quality&quot;: {<br>    &quot;condition&quot;: &quot;good&quot;<br>  }<br>}</pre><p>Claude can now produce a natural-language response such as:</p><blockquote><em>The room is fairly warm at 27.1°C, humidity is moderate, and the air quality looks good.</em></blockquote><p>Then Claude can decide that a visual reaction is appropriate. For example:</p><pre>set_expression(&quot;surprised&quot;)</pre><p>or:</p><pre>set_expression(&quot;happy&quot;)</pre><p>That request follows the same MCP path back to the Raspberry Pi. The display changes expression, while the robot runtime remains responsible for the actual rendering. The complete interaction is:</p><pre>User<br>  ↓<br>Claude Desktop<br>  ↓<br>get_physical_context<br>  ↓<br>Raspberry Pi sensors<br>  ↓<br>semantic context<br>  ↓<br>Claude reasoning<br>  ↓<br>natural-language response<br>  ↓<br>set_expression<br>  ↓<br>Raspberry Pi display</pre><p>This is the pattern I wanted: the AI reasons about the physical world without becoming the hardware controller.</p><h3>Tool discovery should stay dynamic</h3><p>If the bridge hardcodes every Raspberry Pi tool, it becomes tightly coupled to the robot implementation. A better model is:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xo8aIoUUeJ6DwjZYl1_onA.png" /></figure><p>The same applies to tool calls.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2MDMSa_EUXoN5HsG6qbSSA.png" /></figure><p>This means adding a new MCP capability on the Raspberry Pi does not necessarily require changing the bridge.</p><p>The Pi remains the authoritative source of the available MCP interface.</p><h3>Lifecycle ownership matters</h3><p>The architecture I ended up using follows one rule:</p><pre>The Raspberry Pi owns the physical runtime.<br>MCP only exposes it.</pre><p>In one real implementation, PHOS, the process model is effectively:</p><pre>1 Raspberry Pi process<br>1 robot runtime<br>1 application service graph<br>1 semantic MCP surface<br>N MCP client connections</pre><p>An MCP connection never creates another robot runtime. Claude can connect or disconnect without changing the lifecycle of the physical system. That separation becomes increasingly important as more clients are added.</p><h3>Why this architecture works well</h3><p>The main benefit is not simply that Claude Desktop can invoke tools on a Raspberry Pi. The benefit is the boundary. Claude gets a semantic description of the physical system:</p><pre>temperature<br>pressure<br>air quality<br>presence<br>state<br>available actions</pre><p>The Raspberry Pi keeps control over:</p><pre>drivers<br>hardware<br>runtime<br>state<br>rendering<br>lifecycle</pre><p>The result is a clean separation:</p><pre>Claude<br>= reasoning</pre><pre>Raspberry Pi<br>= embodiment</pre><pre>MCP<br>= semantic interface</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1f25a01d469e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/using-mcp-to-control-a-raspberry-pi-from-claude-desktop-1f25a01d469e">Using MCP to Control a Raspberry Pi from Claude Desktop</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Your AI Agent’s Code Is in Git. Its Deployment Isn’t.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/deploy-ai-agents-microsoft-foundry-azd-azure-yaml-9a6f8a05d397?source=rss----29038077e4c6---4"><img src="https://cdn-images-1.medium.com/max/1535/1*bxQW2XC6S8plNAiKTDKlBQ.png" width="1535"></a></p><p class="medium-feed-snippet">How the Microsoft Foundry extension for Azure Developer CLI puts a hosted agent&#x2019;s model, runtime, evaluations, and CI/CD into one&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/codex/deploy-ai-agents-microsoft-foundry-azd-azure-yaml-9a6f8a05d397?source=rss----29038077e4c6---4">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/deploy-ai-agents-microsoft-foundry-azd-azure-yaml-9a6f8a05d397?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/9a6f8a05d397</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[azure]]></category>
            <dc:creator><![CDATA[Dave R | Microsoft Azure & AI MVP ☁️]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 09:27:07 GMT</pubDate>
            <atom:updated>2026-11-10T09:27:06.008Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Web Scraping with Claude Code: A Working Workflow with Gaffa]]></title>
            <link>https://medium.com/codex/web-scraping-with-claude-code-a-working-workflow-with-gaffa-c281a80fb73d?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/c281a80fb73d</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[web-scraping]]></category>
            <category><![CDATA[claude-code]]></category>
            <dc:creator><![CDATA[Kevin Meneses González]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 09:27:02 GMT</pubDate>
            <atom:updated>2026-11-10T09:27:01.008Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0U-vWpQvoqDMlN_ZJ8P6Qw.jpeg" /></figure><p>Scraping scripts rarely fail on day one.</p><p>They fail quietly on day ten.</p><p>The script runs. No exception. A CSV appears. And somewhere in it, 24 rows are missing. Nobody notices until the data is already in a dashboard.</p><p>If you’re:</p><ul><li>collecting product, pricing or catalogue data,</li><li>using Claude Code to write your scrapers,</li><li>or tired of scripts that “work” but return half the data,</li></ul><p>This matters.</p><p>I gave Claude Code a plain-language scraping goal and Gaffa’s AI skills, then let it go from zero to a working web scraping workflow: discovery, a paginated scraper, CSV export and verification. Then I tried to break it.</p><p>Every number, error and output below comes from real runs.</p><h3>The real problem with AI-written scrapers</h3><p>Writing a scraper is the easy part. Claude can do it in one shot.</p><p>The hard part is everything around it:</p><ul><li>Pagination that stops one page early</li><li>Pages that load slowly some of the time</li><li>Errors that only happen once in a while</li></ul><p>None of these throw an error. The script finishes, the CSV looks fine, and the data is wrong.</p><p>You don’t find these bugs by reading code. You find them by running the scraper against the real site and checking the output against something you trust.</p><h3>A scraper needs proof, not just code</h3><p>The fix isn’t a smarter prompt.</p><p>It’s a workflow where the agent can <strong>discover</strong> the site, <strong>build</strong> against the real pages, and <strong>debug</strong> failures from real request data. With checks that tell you when the output is wrong.</p><p>Gaffa’s AI skills cover exactly those three steps:</p><ul><li><strong>/gaffa-find</strong>: give it a URL and a goal, and it explores the site with a credit budget until it finds the answer</li><li><strong>gaffa-authoring</strong>: loads automatically whenever you ask for scraping code, and makes Claude capture the real page before writing a selector</li><li><strong>/gaffa-debug</strong>: pass it a failing request id (brq_...) and it diagnoses what went wrong</li></ul><p>Under the hood, <a href="https://gaffa.dev">Gaffa</a> is a browser automation REST API: you send a URL and a list of actions (click, wait, capture_element, loop...) and it runs them in a real browser.</p><p>👉 You can create an account at <a href="https://gaffa.dev">gaffa.dev</a> and follow along.</p><h3>Setup in one command</h3><p>If you read my previous article on <a href="https://kevinmeneses.com/en/articles/connect-claude-code-gaffa-browser-automation">connecting Claude Code to Gaffa</a>, you already have this. If not, it’s one command in your project folder:</p><pre>npx @gaffa-dev/cli install --tools=claude-code --scope=project -y</pre><pre>Installed the gaffa skills (version 0.9.0) into 1 place:<br>  ./.claude\skills  (project)  gaffa-authoring, gaffa-debug, gaffa-find, gaffa-support</pre><pre>Registered the gaffa-docs MCP server:<br>  Claude Code  ./.mcp.json  added</pre><p>Then set GAFFA_API_KEY as an environment variable. The skills never read the key from anywhere else.</p><p>The target for this test is <a href="https://books.toscrape.com">books.toscrape.com</a>, a sandbox catalogue built for scraping practice: 1,000 books, 50 categories, classic “next” pagination.</p><h3>Phase 1: Discover the site with /gaffa-find</h3><p>I didn’t give Claude a URL for the category, a selector or a page count. Just the homepage and a goal:</p><pre>/gaffa-find https://books.toscrape.com/ — Goal: I want to collect every book<br>in the &#39;Mystery&#39; category. Find the category page URL, how many books it<br>contains, how many listing pages it spans and how pagination works, and which<br>fields each book card exposes (title, price, rating, availability, link).</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bE-2FtGJHak4a3nC.png" /></figure><p><em>Rendered from the real session transcript.</em></p><p>The skill runs a loop: recon, hypothesis, targeted extraction, validation. With hard caps: 15 requests and 50 credits per run, 5 iterations maximum.</p><p>For recon, it sent a browser request that turns the homepage into Markdown. 3 credits, 5.8 seconds, and the answer was in the sidebar:</p><pre>- [Mystery](catalogue/category/books/mystery_3/index.html)</pre><p>From there it captured the category page twice: once as Markdown (to read “32 results” and “Page 1 of 2”) and once as raw HTML (to see the markup the Markdown strips out, like the star rating). Then it checked page 2 to confirm where pagination ends.</p><p>It also wrote a reasoning log to disk, .gaffa-find-&lt;timestamp&gt;.log, one entry per iteration: hypothesis, request id, credits, decision. You can audit exactly how it reached its answer.</p><p>The final result:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Sj54-ZGYnBO8DW_u.png" /></figure><p><em>Rendered from the real session transcript.</em></p><p><strong>Terminal state: success.</strong> 32 books, 2 pages, pagination through a relative li.next a link that disappears on the last page, and a selector for each field. One detail I wouldn&#39;t have caught: the visible title text is truncated with &quot;…&quot;, so the full title has to come from the link&#39;s title attribute.</p><p>Cost of discovery: <strong>9 credits.</strong></p><h3>Phase 2: Turn the findings into a workflow</h3><p>Same Claude session, so it kept everything it had learned. This time I asked for a real workflow:</p><pre>Rewrite it as books_to_csv.py: one single Gaffa browser request that<br>paginates with the loop action. Price as a number, rating as an integer<br>1-5, absolute product URL. Write a CSV. Add --category-url. Run it on<br>Mystery, verify 32 rows with no duplicates, then run it on a category<br>with 3+ pages and verify the count matches the &#39;N results&#39; text.</pre><p>Claude pulled the loop reference through the Gaffa docs MCP server (approved this time), then built the request.</p><h4>One request, every page</h4><p>This is the heart of the workflow. Instead of one request per page, a single request captures the results banner once and then loops: capture the product grid, click “next”, repeat until there is no “next” link.</p><pre>&quot;actions&quot;: [<br>    {&quot;type&quot;: &quot;capture_element&quot;, &quot;selector&quot;: &quot;form.form-horizontal&quot;, &quot;custom_id&quot;: &quot;results-banner&quot;},<br>    {<br>        &quot;type&quot;: &quot;loop&quot;,<br>        &quot;custom_id&quot;: &quot;pagination-loop&quot;,<br>        &quot;max_iterations&quot;: max_pages,<br>        &quot;timeout&quot;: loop_timeout,<br>        &quot;stop_on_fail&quot;: True,<br>        &quot;continue_on_fail&quot;: True,  # the last page has no &quot;next&quot;: that&#39;s how the loop ends<br>        &quot;actions&quot;: [<br>            {&quot;type&quot;: &quot;wait&quot;, &quot;time&quot;: 800, &quot;custom_id&quot;: &quot;settle&quot;},<br>            {&quot;type&quot;: &quot;capture_element&quot;, &quot;selector&quot;: &quot;ol.row&quot;, &quot;custom_id&quot;: &quot;page-cards&quot;},<br>            {&quot;type&quot;: &quot;click&quot;, &quot;selector&quot;: &quot;li.next a&quot;, &quot;custom_id&quot;: &quot;next-page&quot;, &quot;timeout&quot;: 15000},<br>        ],<br>    },<br>],</pre><p>Parsing happens in plain Python with regular expressions over the captured HTML. No LLM extraction. The skill steers Claude away from Gaffa’s parse_json action for structured pages like this one, because a deterministic parser is cheaper and returns the same output every run.</p><h4>The check that caught a bug</h4><p>The most valuable line in the script is not the scraper. It’s this:</p><pre>status = &quot;OK&quot; if reported_total == len(books) else &quot;MISMATCH&quot;<br>print(f&quot;page says {reported_total} results; scraped {len(books)} -&gt; {status}&quot;)<br>if status == &quot;MISMATCH&quot;:<br>    sys.exit(1)</pre><p>The workflow compares its own output with the number the website reports. If they differ, it fails loudly.</p><p>Here is how phase 2 went:</p><p><strong>Attempt 1: Mystery.</strong> 32 of 32 books, 32 unique URLs, banner matched. 3 credits.</p><p><strong>Attempt 2: Nonfiction (110 books, 6 pages).</strong></p><pre>request: brq_VzZhqRu7EsTn47GhMyLqkSN76iJDA6<br>pages captured: 6<br>books written: 86 -&gt; nonfiction.csv<br>page says 110 results; scraped 86 -&gt; MISMATCH</pre><p>All 6 pages were captured, but 24 books were missing. Claude broke it down page by page:</p><pre>page 1: articles=20<br>page 2: articles=20<br>page 3: articles=20<br>page 4: articles=12<br>page 5: articles=14<br>page 6: articles=2</pre><p>Pages 4 to 6 were captured while the browser was still loading them, right after the click. Claude added a short wait at the start of each iteration and re-ran: <strong>110 of 110</strong>. Then re-ran Mystery to make sure nothing regressed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HvQasUI5Qr3zKca3.png" /></figure><p><em>Rendered from the real session transcript.</em></p><p>Without that count check, the script would have exited 0 and handed me a CSV missing 22% of the data.</p><p>Cost of phase 2: <strong>6 billed requests, 18 credits.</strong></p><h3>Then I tried to break it</h3><p>Claude said it was done. I didn’t take its word for it. I ran the script myself on categories it had never seen.</p><p>Travel (11 books) and Fiction (65 books, 4 pages) both matched their counts. Then I ran Nonfiction again:</p><pre>pages captured: 2<br>books written: 40 -&gt; nonfiction.csv<br>page says 110 results; scraped 40 -&gt; MISMATCH</pre><p>The loop stopped after page 2. The count check caught it again.</p><p>Failed and partial requests are part of scraping any live website. Pages get slow, layouts change, content loads late. What matters is how quickly you find out why. That’s the job of the third skill.</p><h3>Phase 3: Diagnose with /gaffa-debug</h3><p>I passed the request id to Claude:</p><pre>/gaffa-debug brq_VzZj6m1Mqsb31bZEevh2ZHdUNVFxyh — it only got 40 of 110<br>books (MISMATCH). Diagnose why the loop stopped early, apply a minimal fix<br>that does not rely on luck, then verify on Nonfiction twice and Mystery once.</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DQwWApzkIChyU0lD.png" /></figure><p><em>Rendered from the real session transcript.</em></p><p>The diagnosis was precise:</p><ol><li>The request data showed the loop ran <strong>2 iterations</strong>, and the second “next” click failed with action_timed_out.</li><li>Was page 2 really the last page? Claude fetched page-2.html directly (free, no Gaffa call). It has a &quot;next&quot; link to page 3. <strong>So the loop stopped early by mistake.</strong></li><li>Why? On that run the target page took <strong>27.9 seconds</strong> to load, against about 2 seconds on every other run. Live sites have slow moments, and the click only waited 5 seconds before giving up.</li></ol><p>The patch was one value: raise the click’s timeout from 5,000 to 15,000 ms, the same value Gaffa&#39;s own pagination examples use. A real last page still ends the loop, just 15 seconds later.</p><p>Verification: Nonfiction 110 of 110 twice in a row with identical rows, Mystery 32 of 32. <strong>13 credits.</strong></p><h3>The final result</h3><p>I ran the final script one last time on my own:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*FUz14n-zFXhDtQOU.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/872/1*VIwbBeCqVecjH_arBXspEA.png" /></figure><p><strong>218 books, 4 categories, 0 duplicates.</strong> Nonfiction, 6 pages in a single request, takes about 42 seconds end to end.</p><p>The CSV is clean, typed data ready for pandas or a spreadsheet:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Xafd7Kh9kiZdOFUV.png" /></figure><pre>title,price,rating,availability,url<br>Worlds Elsewhere: Journeys Around Shakespeare’s Globe,40.3,5,In stock,https://books.toscrape.com/catalogue/worlds-elsewhere-journeys-around-shakespeares-globe_972/index.html<br>The Five Love Languages: How to Express Heartfelt Commitment to Your Mate,31.05,3,In stock,https://books.toscrape.com/catalogue/the-five-love-languages-how-to-express-heartfelt-commitment-to-your-mate_969/index.html<br>Reasons to Stay Alive,26.41,2,In stock,https://books.toscrape.com/catalogue/reasons-to-stay-alive_959/index.html</pre><h4>What the whole thing cost</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/872/1*CVxHiol5Ab3_ao9WSggvoA.png" /></figure><p>Claude Code ran on my Claude Pro subscription.</p><p>From here you can:</p><ul><li>schedule it with cron or Windows Task Scheduler, since it exits 1 on a mismatch</li><li>loop over all 50 category URLs to build the full 1,000-book catalogue</li><li>point the same pattern at any paginated listing: job boards, product grids, directories</li></ul><h3>What I’d do differently next time</h3><p>Three lessons from this run, all of which apply outside Gaffa:</p><p><strong>Put the verification in the script, not in your head.</strong> The “N results” check caught two silent failures that would otherwise have shipped. If the site tells you how many items it has, use it.</p><p><strong>Debug from request data, not from guesses.</strong> /gaffa-debug didn&#39;t guess. It read how many iterations ran, which action failed and how long the page took to load, then checked the live page before changing a single value.</p><p><strong>Fixed waits are guesses.</strong> The 800 ms wait happened to work, and the 5 s click timeout didn&#39;t when the site was slow. When you can, wait on a selector or give timeouts real margin.</p><h3>Key takeaways</h3><ul><li>/gaffa-find turned &quot;collect every Mystery book&quot; into URLs, selectors and pagination rules in 9 credits.</li><li>One Gaffa request with loop scraped a 6-page category in about 42 seconds, and a built-in count check caught two silent data losses.</li><li>/gaffa-debug traced a stopped loop to a 27.9-second page load and fixed it with a single timeout change.</li></ul><h3>Final thoughts</h3><p>Claude Code writes scrapers fast. That was never the bottleneck.</p><p>The bottleneck was knowing whether the data is right.</p><p>Give the agent a real browser, skills that force it to look before it writes, and a workflow that checks its own output, and web scraping with Claude Code becomes something you can actually trust.</p><p>👉 Try it: create an account at <a href="https://gaffa.dev">gaffa.dev</a>, run npx @gaffa-dev/cli install, and start with /gaffa-find on a site you need data from.</p><p><em>Originally published at </em><a href="https://kevinmeneses.com/en/articles/web-scraping-claude-code-gaffa-workflow"><em>kevinmeneses.com</em></a><em> on November 6, 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c281a80fb73d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/web-scraping-with-claude-code-a-working-workflow-with-gaffa-c281a80fb73d">Web Scraping with Claude Code: A Working Workflow with Gaffa</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Enable a Spring Boot Application to Handle 2 Million Requests Per Second]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/how-to-enable-a-spring-boot-application-to-handle-2-million-requests-per-second-8d979a1d7462?source=rss----29038077e4c6---4"><img src="https://cdn-images-1.medium.com/max/2600/0*_KybajUhrYMEf991" width="5432"></a></p><p class="medium-feed-snippet">Spring Boot: Handle 2 Million Requests Per Second</p><p class="medium-feed-link"><a href="https://medium.com/codex/how-to-enable-a-spring-boot-application-to-handle-2-million-requests-per-second-8d979a1d7462?source=rss----29038077e4c6---4">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/how-to-enable-a-spring-boot-application-to-handle-2-million-requests-per-second-8d979a1d7462?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/8d979a1d7462</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Riya Sharma]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 09:26:57 GMT</pubDate>
            <atom:updated>2026-11-10T09:26:55.938Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Stop replacing Makefiles with Justfiles — You are solving the wrong problem]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/stop-replacing-makefiles-with-justfiles-you-are-solving-the-wrong-problem-f29e4bcf821c?source=rss----29038077e4c6---4"><img src="https://cdn-images-1.medium.com/max/2600/0*0KLX-aRDPMA2N5-R" width="5006"></a></p><p class="medium-feed-snippet">Why switching build tools won&#x2019;t necessarily fix a broken development workflow</p><p class="medium-feed-link"><a href="https://medium.com/codex/stop-replacing-makefiles-with-justfiles-you-are-solving-the-wrong-problem-f29e4bcf821c?source=rss----29038077e4c6---4">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/stop-replacing-makefiles-with-justfiles-you-are-solving-the-wrong-problem-f29e4bcf821c?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/f29e4bcf821c</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[writing]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[golang]]></category>
            <dc:creator><![CDATA[Yaninyz witty]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 09:26:47 GMT</pubDate>
            <atom:updated>2026-11-10T09:26:46.456Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[MIT Let GPT-6 Edit Another AI’s Brain. Its Math Score Jumped Sixfold.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/mit-let-gpt-6-edit-another-ais-brain-its-math-score-jumped-sixfold-a30bc3210a5b?source=rss----29038077e4c6---4"><img src="https://cdn-images-1.medium.com/max/1600/1*S0_XgHE_Ipq7i9NctEXkiA.jpeg" width="1600"></a></p><p class="medium-feed-snippet">The AI was getting stuck in its own thoughts. Give it a maths problem and it would start working, repeat itself, change its mind, repeat&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/codex/mit-let-gpt-6-edit-another-ais-brain-its-math-score-jumped-sixfold-a30bc3210a5b?source=rss----29038077e4c6---4">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/mit-let-gpt-6-edit-another-ais-brain-its-math-score-jumped-sixfold-a30bc3210a5b?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/a30bc3210a5b</guid>
            <category><![CDATA[ai-research]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[generative-ai]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[MayhemCode]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 09:26:44 GMT</pubDate>
            <atom:updated>2026-11-10T09:26:42.796Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Stop Testing Email with Production SMTP: 5 Free Alternatives for Developers]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/stop-testing-email-with-production-smtp-5-free-alternatives-for-developers-4e47f889a9c9?source=rss----29038077e4c6---4"><img src="https://cdn-images-1.medium.com/max/1672/1*jBeHYoFmvTGg1mO5WmdPPA.png" width="1672"></a></p><p class="medium-feed-snippet">You just finished building a password reset feature.</p><p class="medium-feed-link"><a href="https://medium.com/codex/stop-testing-email-with-production-smtp-5-free-alternatives-for-developers-4e47f889a9c9?source=rss----29038077e4c6---4">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/stop-testing-email-with-production-smtp-5-free-alternatives-for-developers-4e47f889a9c9?source=rss----29038077e4c6---4</link>
            <guid isPermaLink="false">https://medium.com/p/4e47f889a9c9</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[php]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[web-development]]></category>
            <dc:creator><![CDATA[Developer Awam]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 09:26:27 GMT</pubDate>
            <atom:updated>2026-11-10T09:26:26.678Z</atom:updated>
        </item>
    </channel>
</rss>