<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Noel Himer</title>
    <description>The latest articles on DEV Community by Noel Himer (@unbearablelabs).</description>
    <link>https://dev.to/unbearablelabs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3957067%2F7aeaf394-3ab1-4e17-bab7-3ebcea759137.png</url>
      <title>DEV Community: Noel Himer</title>
      <link>https://dev.to/unbearablelabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/unbearablelabs"/>
    <language>en</language>
    <item>
      <title>I gave my AI workers a cited knowledgebase so they'd stop guessing</title>
      <dc:creator>Noel Himer</dc:creator>
      <pubDate>Thu, 18 Jun 2026 21:25:01 +0000</pubDate>
      <link>https://dev.to/unbearablelabs/i-gave-my-ai-workers-a-cited-knowledgebase-so-theyd-stop-guessing-2ndk</link>
      <guid>https://dev.to/unbearablelabs/i-gave-my-ai-workers-a-cited-knowledgebase-so-theyd-stop-guessing-2ndk</guid>
      <description>&lt;p&gt;My agents were confidently wrong about the world, and I couldn't tell when. That's the part that got to me — not the wrongness, the &lt;em&gt;confidence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I run my one-person company as a fleet of about twenty AI agents — a content writer, a finance one, a researcher, a security officer, a handful more. They're good at the work I built them for. But every one of them shares a flaw I'd been papering over: when a task needs a fact about the world — how a tax threshold works, what a marketing framework actually says, how a platform bills — the model reaches into its training data and answers in the exact same self-assured tone whether it knows or is improvising. There is no tell. The guess and the fact wear the same face.&lt;/p&gt;

&lt;p&gt;So this month I built the thing that was missing: a cited, fact-checked knowledgebase the agents have to read &lt;em&gt;before&lt;/em&gt; they work, with a gate that keeps me from poisoning my own source of truth. Here's how it's built, the one rule that turned out to matter most, and the honest state of it — which is that I finished it days ago and have no idea yet whether it changes the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job I was actually hiring this to do
&lt;/h2&gt;

&lt;p&gt;Strip away my setup and the problem is one any solo operator using AI already has. You ask the model for something that depends on a real fact. It answers fluently. You either know enough to catch the error or you don't — and the whole reason you're asking is usually that you don't. The job I needed done wasn't "make my agents smarter." It was narrower and more honest: &lt;strong&gt;stop my AI from making things up in the one register where I can't catch it, and let me know which claims I can actually trust.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The competition for that job, in my shop, was "just let the model wing it and hope." That had already cost me. A marketing analysis once understated a channel's numbers because an agent trusted a stale figure instead of pulling the live one. Small, recoverable — but it's the recoverable ones you see. The ones you don't see are the ones that scare you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;It's a region of my notes vault I call the 10-Library — the &lt;em&gt;factual&lt;/em&gt; half, deliberately walled off from the operational logbook (sessions, decisions, day-to-day state). One test decides what's allowed in: &lt;strong&gt;"Would this still be true if my company vanished tomorrow?"&lt;/strong&gt; A fact about a copywriting framework: yes, it lives here. A fact about my own revenue: no, that's operational memory. The Library only holds world-true things.&lt;/p&gt;

&lt;p&gt;The concrete shape, as of today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;110 notes&lt;/strong&gt;, each one atomic — a single concept, not a topic dump — across five categories so far: infrastructure, Linux, networking, distribution (marketing), and freelancing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claim-level sourcing.&lt;/strong&gt; Every factual sentence carries an inline citation to where it came from. No source, no entry. A note without a footnote isn't a Library fact; it's an opinion, and it doesn't get to sit in the source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A quarantine.&lt;/strong&gt; New facts — researched from the web on a schedule, or pulled on demand when an agent hits a gap — don't land in the trusted set. They land in a &lt;code&gt;_quarantine&lt;/code&gt; folder marked &lt;code&gt;auto-unverified&lt;/code&gt;, and they stay invisible to the agents until &lt;em&gt;I&lt;/em&gt; read them and promote them by hand. Right now there's exactly one note sitting in quarantine, waiting on me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The notes are distilled from sources I'd actually defend — accredited course material and primary references, not a model free-associating about itself. The marketing notes I leaned on to &lt;em&gt;write this very essay&lt;/em&gt;, for instance, cite Ogilvy's own books and a 1994 psychology paper, not a listicle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule that mattered most
&lt;/h2&gt;

&lt;p&gt;If I keep only one sentence from this build, it's this: &lt;strong&gt;the agent reads the knowledgebase, but the agent never gets to silently rewrite it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This sounds like bureaucratic caution. It's the opposite — it's the thing that keeps the whole structure from rotting. There's a documented failure mode where a language model asked to re-verify its own knowledge base will quietly &lt;em&gt;degrade&lt;/em&gt; correct facts and ratify its own errors, all while looking like diligent maintenance. An AI grading its own homework drifts, and drifts confidently. So in my Library, automated re-checking is advisory and append-only: it can flag a note as stale or contradicted, it can stage a proposed change for me to look at — but it cannot overwrite a single fact on its own. The human promote step is not a nicety. It's the load-bearing wall.&lt;/p&gt;

&lt;p&gt;The second half of the same rule: ingested web content is treated as &lt;strong&gt;data, never instructions.&lt;/strong&gt; A note pulled from the internet can inform an answer; it can never trigger an action — no write, no tool call, no spend — without me confirming first. That single boundary quietly closes off the nightmare where someone poisons a web page my researcher reads and my fleet starts &lt;em&gt;acting&lt;/em&gt; on the poison. The agents read the world. They don't take orders from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;It is not RAG-makes-the-AI-correct. A knowledgebase doesn't make a model truthful; it gives me a &lt;em&gt;bounded, citable place&lt;/em&gt; where I've decided what's true, so that for the facts that matter I'm relying on something I vetted instead of something the model felt confident about. The gap between those two is the entire point. Outside the Library, my agents are exactly as fallible as yours. Inside it, at least, when one cites a fact I can click the footnote.&lt;/p&gt;

&lt;p&gt;And it is brand new. I'm not going to tell you it made the fleet measurably better, because I genuinely don't know yet — it's been live for days, not months. What I can tell you is the failure it's designed to stop is real, I've been bitten by the cheap version of it, and the design is the one I'd defend: read before you work, cite every claim, and never let the machine quietly edit the truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest status, as always
&lt;/h2&gt;

&lt;p&gt;This is the part of every one of these I refuse to skip, because the honesty is the actual product. Revenue is still zero. The audience is still tiny. The knowledgebase I just described was &lt;em&gt;built this week&lt;/em&gt; — 110 notes is a respectable start and also nowhere near a finished reference; whole categories I sketched are still empty. The most alive thing in the whole operation remains the operating system itself plus one warm commission: a small desktop app I'm making for my mother, who'll pass it by word of mouth to the people she works with.&lt;/p&gt;

&lt;p&gt;So treat this as a build log, not a victory lap. I built a cited knowledgebase so my AI workers would stop guessing in the register where I can't catch them, and I wired in the one rule — &lt;em&gt;read it, don't rewrite it&lt;/em&gt; — that keeps me from becoming the thing that corrupts it. Whether it earns its keep, I'll find out in public and tell you either way.&lt;/p&gt;

&lt;p&gt;If you use AI for anything load-bearing, here's the cheap version you can steal today: keep one file of facts you've personally checked, cite where each came from, point the model at it before it answers, and never let it edit that file without you reading the change. You don't need a vault or twenty agents. You need a place where "true" means &lt;em&gt;you&lt;/em&gt; decided it, not the model.&lt;/p&gt;




&lt;p&gt;I write up the operating system one piece at a time — the agent wiring, the failures, the rules I reverse — in &lt;a href="https://unbearabletechtips.beehiiv.com/?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=operator-ai-org-4"&gt;&lt;strong&gt;Unbearable TechTips Weekly&lt;/strong&gt;&lt;/a&gt;. It's practical homelab and agent ops, the real status included. If the mess is useful to you, that's where the specifics live.&lt;/p&gt;

&lt;p&gt;— Noel @ Unbearable Labs&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>buildinpublic</category>
      <category>programming</category>
    </item>
    <item>
      <title>I run my one-person company as a ~20-agent AI org. Here's what broke.</title>
      <dc:creator>Noel Himer</dc:creator>
      <pubDate>Thu, 04 Jun 2026 14:04:19 +0000</pubDate>
      <link>https://dev.to/unbearablelabs/i-run-my-one-person-company-as-a-20-agent-ai-org-heres-what-broke-2ik4</link>
      <guid>https://dev.to/unbearablelabs/i-run-my-one-person-company-as-a-20-agent-ai-org-heres-what-broke-2ik4</guid>
      <description>&lt;p&gt;I'm a solo operator — no employees, no co-founder. But I don't really work alone anymore. I work as the human in the loop of a small org made of about twenty specialized AI agents, each with a job, a name, and a list of things it is explicitly &lt;em&gt;not&lt;/em&gt; allowed to do.&lt;/p&gt;

&lt;p&gt;This isn't a "10x your output with AI" post. The most valuable thing the setup taught me is that I'd spent weeks building the wrong things — I'll get to that. But first the shape, because people keep asking what "running your company with agents" actually means once you get past the screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;chief-of-staff&lt;/strong&gt; agent runs every morning and writes me a brief: what shipped in the last 24 hours, what's gone stale, what's about to cross a limit I set. A &lt;strong&gt;finance&lt;/strong&gt; agent knows my tax situation. A &lt;strong&gt;security officer&lt;/strong&gt; scans for leaked secrets before anything gets committed. A &lt;strong&gt;researcher&lt;/strong&gt; validates ideas — and is deliberately rate-limited, so it can't start a new investigation until the last one produced something real. A &lt;strong&gt;shipper&lt;/strong&gt; scaffolds and deploys. There's a content lead, a publisher, an archivist, and a dozen more.&lt;/p&gt;

&lt;p&gt;They share three things: one task backlog that is the single source of truth for what's being worked on, a written knowledge vault so the next agent — or the next session — inherits full context instead of starting blind, and a set of real tools behind one endpoint, so an agent can actually &lt;em&gt;do&lt;/em&gt; things rather than just describe them.&lt;/p&gt;

&lt;p&gt;Over the top sits a console I built called URSA. It's the cockpit — live status, and a voice. Literally a voice: it speaks to me in two languages, and it interrupts me, unprompted, when a number I told it to watch goes the wrong way.&lt;/p&gt;

&lt;p&gt;Two rules hold the whole thing together. &lt;strong&gt;Anything that spends money, publishes to the world, or touches an account stops and asks me first.&lt;/strong&gt; And &lt;strong&gt;every product gets a kill threshold the day it ships&lt;/strong&gt; — a pre-committed number and date at which I walk away — so a thing that isn't working can't quietly absorb my attention forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;Three things, in order of how much they taught me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One: an agent hung for eight hours.&lt;/strong&gt; I asked a build agent to test a service. Instead of starting it in the background and probing it, it ran the server in the &lt;em&gt;foreground&lt;/em&gt; — then sat there, blocked, holding the process open for 472 minutes, spawning orphans the entire time. The lesson is now a rule: an agent must never foreground-run a daemon, and the orchestrator has to watchdog its workers on a timer. Agents fail in ways a human just wouldn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two: a config file got silently overwritten.&lt;/strong&gt; For a stretch, builds were mysteriously breaking. The cause: an agent had written test-fixture content directly over a real Dockerfile, and nothing caught it because that directory wasn't even tracked. The fix was a guard that hashes the file and fails the commit if it changed unexpectedly. The deeper lesson: agents need guardrails that fail &lt;em&gt;loudly, at the moment of the mistake&lt;/em&gt; — not a code review three steps downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three — the expensive one: I built seven products before a single person asked for one.&lt;/strong&gt; This is the one I'm still sitting with. My fleet is &lt;em&gt;good&lt;/em&gt; at building. In a few weeks it shipped seven real tools — tested, deployed, the works. And I use them. But I built every one because the market &lt;em&gt;looked&lt;/em&gt; like it was there, never because a real person pulled it out of me. Meanwhile the only things that feel genuinely valuable are the internal tools — the console, the org itself — which exist because I actually needed them.&lt;/p&gt;

&lt;p&gt;The pattern took me embarrassingly long to see: everything I built from a &lt;em&gt;push&lt;/em&gt; ("this looks like a market") came out hollow. Everything I built from a &lt;em&gt;pull&lt;/em&gt; (a need I already had) came out alive. So I made the org enforce it. There's now a gate every new build has to clear before it gets any of my time, and its first question is the one I'd been skipping — &lt;em&gt;who pulled this?&lt;/em&gt; One of my agents is read-only and exists for essentially that purpose: to tell me no.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try this yourself
&lt;/h2&gt;

&lt;p&gt;You don't need twenty agents. Almost all of the value came from four prompt patterns, and they fit inside a single system prompt — paste them into whatever AI you already use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Give it a role and a "never" list.&lt;/strong&gt; The negative space does more work than the instructions. What makes an agent safe isn't what you tell it to do — it's what you forbid.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;You are my [role]. Before any action, check it against these hard limits: never spend money, publish anything public, or touch an account without stopping to ask me first. If a task would cross one of those lines, stop and surface it instead of proceeding.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. Make it argue back before it builds.&lt;/strong&gt; My single most useful agent is read-only and exists to say no. You can bake the same reflex into one prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Before you build, fix, or write anything, ask one question first: who actually asked for this? If the honest answer is "no one, it just seems like a good idea," say so and push back before doing the work. Default to "let's validate first," never "let's build."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Give it a memory it reads before it starts.&lt;/strong&gt; Keep one file of state — what you're working on, what's decided, what you've already tried — and begin every session by pointing the AI at it. Context that lives in a file survives; context that lives in a chat window dies when you close it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;At the start of every session, read STATE.md before doing anything. Whenever something is decided or changes, write it back. Never make me repeat myself.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4. Pre-commit the kill line.&lt;/strong&gt; The discipline that's saved me the most: decide the failure condition before you start, not after you're attached.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;For anything we start, write down now the condition under which we stop — a number and a date. When we hit it, remind me and make me defend continuing.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the spine. Twenty specialized agents with real tools is just this, scaled out — but the four patterns are the part that actually changed how I work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I still don't know
&lt;/h2&gt;

&lt;p&gt;Whether any of this is a business, or just an elaborate and deeply satisfying way to run a one-person shop. Right now I have a fleet of tools I use every day and a customer count I'm still working on. That's the honest status. I'm betting the discipline I just bolted on is what turns the second number — but I don't know yet, and I'd rather say that than pretend.&lt;/p&gt;

&lt;p&gt;If the wiring behind any of this is interesting to you — how the agents coordinate, how the console got a voice, how the kill thresholds actually fire — that's what I write up, one build at a time, in &lt;a href="https://unbearabletechtips.beehiiv.com/?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=operator-ai-org"&gt;&lt;strong&gt;Unbearable TechTips Weekly&lt;/strong&gt;&lt;/a&gt;. The architecture's there, the war-stories are there, and I'm figuring out the rest in public.&lt;/p&gt;

&lt;p&gt;— Noel, Unbearable Labs&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Two supply-chain attacks in one week — here's what to actually fix in your CI</title>
      <dc:creator>Noel Himer</dc:creator>
      <pubDate>Tue, 02 Jun 2026 18:06:35 +0000</pubDate>
      <link>https://dev.to/unbearablelabs/two-supply-chain-attacks-in-one-week-heres-what-to-actually-fix-in-your-ci-2knc</link>
      <guid>https://dev.to/unbearablelabs/two-supply-chain-attacks-in-one-week-heres-what-to-actually-fix-in-your-ci-2knc</guid>
      <description>&lt;p&gt;On May 18, 2026, between 11:36 and 17:48 UTC, the TeamPCP threat group compromised 5,561 public GitHub repositories in six hours. They pushed malicious GitHub Actions workflows via stolen developer credentials — injecting new workflows or replacing existing ones with dormant &lt;code&gt;workflow_dispatch&lt;/code&gt; backdoors. Every repository that ran those workflows handed over whatever secrets the CI environment held: AWS keys, GCP tokens, SSH keys, Docker auth configs, Kubernetes credentials. (&lt;a href="https://www.securityweek.com/over-5500-github-repositories-infected-in-megalodon-supply-chain-attack/" rel="noopener noreferrer"&gt;SecurityWeek&lt;/a&gt;, &lt;a href="https://safedep.io/megalodon-mass-github-repo-backdooring-ci-workflows/" rel="noopener noreferrer"&gt;SafeDep&lt;/a&gt;, &lt;a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-shai-hulud-megalodon-supply-chain-cascade/" rel="noopener noreferrer"&gt;CSA Labs&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Ten days later: Microsoft published a report on 14 typosquatted npm packages, stealing cloud and CI/CD secrets via malicious &lt;code&gt;postinstall&lt;/code&gt; scripts. Different actor, same threat category. (&lt;a href="https://www.microsoft.com/en-us/security/blog/2026/05/28/typosquatted-npm-packages-used-steal-cloud-ci-cd-secrets/" rel="noopener noreferrer"&gt;Microsoft Security Blog&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Both attacks exploited structural misconfigurations that exist in most GitHub Actions setups today. Not zero-days. Configuration patterns that look completely normal to anyone who learned CI/CD from the standard tutorials.&lt;/p&gt;

&lt;p&gt;This post covers what those patterns are and how to fix them. The five fixes are free. At the end I'll mention a starter kit that implements them — but the fixes work regardless.&lt;/p&gt;




&lt;h2&gt;
  
  
  The EU CRA angle: this is now a compliance problem too
&lt;/h2&gt;

&lt;p&gt;The EU Cyber Resilience Act enters its reporting obligations phase in September 2026. Article 13 requires manufacturers of "products with digital elements" to document a software bill of materials and maintain verifiable build provenance. Mutable GitHub Actions tags (&lt;code&gt;@v4&lt;/code&gt;, &lt;code&gt;@main&lt;/code&gt;) produce builds you cannot verify after the fact — you cannot prove which code ran.&lt;/p&gt;

&lt;p&gt;SHA-pinned actions plus SLSA provenance attestation (Fix 3 below includes this) is how you produce a build you can actually audit. If you ship software to EU customers, these fixes are not just security hygiene. They are the start of a compliance trail.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two misconfigurations that made Megalodon work
&lt;/h2&gt;

&lt;p&gt;Megalodon's payload executed because of two conditions: (1) the attacker could push a workflow file into the repository, and (2) when it ran, it had access to secrets. The stolen credentials angle is an endpoint problem CI hardening cannot solve. But the CI side has real mitigations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mutable action refs gave attackers a template to work from.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most workflows look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;some-third-party/action@v2.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@v4&lt;/code&gt;, &lt;code&gt;@v5&lt;/code&gt;, &lt;code&gt;@v2.1&lt;/code&gt; are tags. Tags are mutable. A publisher can update what a tag points to at any time. Preceding Megalodon, TeamPCP had already demonstrated the technique: force-pushing malicious commits to tag refs on popular action repos, causing downstream consumers to execute attacker code on their next CI run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;permissions: write-all&lt;/code&gt; gave the attacker the run of the house.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a workflow has broad permissions and an attacker can inject a step — via a compromised action, via a &lt;code&gt;pull_request_target&lt;/code&gt; misconfiguration, or via stolen credentials — that injected step has the same permissions as the workflow. If the workflow has &lt;code&gt;permissions: write-all&lt;/code&gt;, the attacker can push code, create releases, and exfiltrate every secret in scope.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 1: SHA-pin your actions
&lt;/h2&gt;

&lt;p&gt;The only immutable ref format is a 40-character commit SHA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Before:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

  &lt;span class="c1"&gt;# After:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt;  &lt;span class="c1"&gt;# v4.2.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SHA cannot be retroactively changed. Your workflow runs the pinned version until you explicitly update.&lt;/p&gt;

&lt;p&gt;Finding the correct SHA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh api repos/actions/checkout/git/refs/tags/v4.2.2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.object.sha'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Staying current without manual SHA hunting:&lt;/strong&gt; add Dependabot for GitHub Actions to &lt;code&gt;.github/dependabot.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;updates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;package-ecosystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github-actions"&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly"&lt;/span&gt;
    &lt;span class="na"&gt;commit-message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ci(deps)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependabot opens PRs when new action versions ship. Review the diff, merge, done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 2: Scope your permissions
&lt;/h2&gt;

&lt;p&gt;Set a read-only default at the workflow level, expand per-job where needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Top of every workflow file:&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="c1"&gt;# Read-only inherited — nothing to add&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ... deploy steps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an attacker injects a step into &lt;code&gt;test&lt;/code&gt;, it has read-only access. It cannot push code or exfiltrate secrets via repository writes. Two minutes per workflow. Highest value-to-effort of the five fixes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 3: Eliminate &lt;code&gt;pull_request_target&lt;/code&gt; + checkout by PR SHA
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pull_request_target&lt;/code&gt; runs with the base repository's permissions and secrets, even when triggered by a fork's PR. Combined with checking out the PR's code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# THIS IS DANGEROUS — do not use:&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request_target&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@&amp;lt;sha&amp;gt;&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;  &lt;span class="c1"&gt;# attacker-controlled&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;  &lt;span class="c1"&gt;# runs attacker code with your secrets&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone who opens a PR from a fork controls what &lt;code&gt;checkout&lt;/code&gt; retrieves. Use &lt;code&gt;pull_request&lt;/code&gt; (not &lt;code&gt;pull_request_target&lt;/code&gt;) for standard CI. If you need &lt;code&gt;pull_request_target&lt;/code&gt; for auto-labeling, do not checkout the PR head code in the same job.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 4: Move event context values through &lt;code&gt;env:&lt;/code&gt; before using in &lt;code&gt;run:&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before (dangerous — script injection possible):&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Process PR&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "PR by&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.user.login }}"&lt;/span&gt;

&lt;span class="c1"&gt;# After (safe):&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Process PR&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;PR_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.user.login }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "PR by&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$PR_USER"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;github.event.pull_request.user.login&lt;/code&gt; is attacker-controlled. Direct interpolation into &lt;code&gt;run:&lt;/code&gt; allows shell metacharacter injection. The same applies to &lt;code&gt;pull_request.title&lt;/code&gt;, &lt;code&gt;pull_request.body&lt;/code&gt;, &lt;code&gt;issue.title&lt;/code&gt;, &lt;code&gt;head_commit.message&lt;/code&gt;, and any other user-controlled webhook payload value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 5: Audit npm dependencies for postinstall scripts
&lt;/h2&gt;

&lt;p&gt;The May 28 Microsoft report is a reminder that &lt;code&gt;npm install&lt;/code&gt; in CI is a supply-chain boundary. All 14 malicious packages used &lt;code&gt;postinstall&lt;/code&gt; scripts to exfiltrate credentials — scripts that run automatically during &lt;code&gt;npm install&lt;/code&gt; with no confirmation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Lockfile-exact install, no drift:&lt;/span&gt;
npm ci

&lt;span class="c"&gt;# Disable postinstall for trusted-dependency installs:&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;

&lt;span class="c"&gt;# Audit which packages in your tree have postinstall scripts:&lt;/span&gt;
npm &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--parseable&lt;/span&gt; | xargs &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"
  try {
    const p = require('{}/package.json');
    if (p.scripts &amp;amp;&amp;amp; p.scripts.postinstall) {
      console.log('postinstall found in:', '{}');
    }
  } catch(e) {}
"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--ignore-scripts&lt;/code&gt; breaks packages that need postinstall to compile native extensions. In that case, review and explicitly allow those specific packages rather than turning the flag off globally.&lt;/p&gt;




&lt;h2&gt;
  
  
  What these five fixes add up to
&lt;/h2&gt;

&lt;p&gt;SHA-pinned actions, scoped permissions, no &lt;code&gt;pull_request_target&lt;/code&gt; + checkout misuse, &lt;code&gt;env:&lt;/code&gt; for event context, and postinstall awareness do not make your CI invulnerable. They remove the structural misconfigurations that make attacks like Megalodon consequential.&lt;/p&gt;

&lt;p&gt;If an attacker pushes a malicious workflow (via stolen credentials — the endpoint problem), a SHA-pinned, permission-scoped workflow limits what it can do when it runs. That is the goal.&lt;/p&gt;




&lt;h2&gt;
  
  
  The starter kit (coming soon)
&lt;/h2&gt;

&lt;p&gt;These five fixes — plus Trivy scanning, cosign keyless image signing, SLSA Level 1 provenance attestation, TruffleHog secret scanning on PRs, and a scheduled Dependabot config — are what I'm packaging into ready-to-paste GitHub Actions workflow templates.&lt;/p&gt;

&lt;p&gt;What's planned for the pack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ci-base-hardened.yml&lt;/code&gt; — minimal hardened starter for any stack&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ci-node-hardened.yml&lt;/code&gt; — Node.js with &lt;code&gt;npm ci&lt;/code&gt;, postinstall audit, &lt;code&gt;npm audit&lt;/code&gt; gate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ci-python-hardened.yml&lt;/code&gt; — Python with pip-audit against OSV + PyPI advisory DB&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ci-docker-hardened.yml&lt;/code&gt; — Docker build with Trivy scan, cosign keyless signing, SBOM attestation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ci-release-hardened.yml&lt;/code&gt; — release workflow with SLSA Level 1 provenance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ci-pr-gate.yml&lt;/code&gt; — PR gate with dependency-review, TruffleHog, mutable-ref detector&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ci-schedule-audit.yml&lt;/code&gt; — weekly scheduled audit via the github-actions-audit MCP tool&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dependabot-actions.yml&lt;/code&gt; — keeps SHA pins current via automated PRs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;security-policy-template.md&lt;/code&gt; — SECURITY.md so researchers don't open public issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop them into &lt;code&gt;.github/workflows/&lt;/code&gt;, follow the per-file setup instructions, and you'd have a hardened baseline in under an hour. It'll be &lt;strong&gt;€19, one payment, no subscription&lt;/strong&gt; — but it's not out yet. I'm gauging interest before I finish it: claim a free spot and I'll send one email the day it ships, plus early-bird pricing for list members.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://unbearable0.gumroad.com/l/hardened-ci-pack-waitlist?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=n3-hardened-ci-teaser" rel="noopener noreferrer"&gt;Join the free early-access list →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Noel @ &lt;a href="https://unbearabletechtips.beehiiv.com?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=n3-hardened-ci-teaser" rel="noopener noreferrer"&gt;Unbearable Labs&lt;/a&gt;. The newsletter covers practical homelab and agent ops — weekly, no filler.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>github</category>
      <category>cicd</category>
    </item>
    <item>
      <title>I shipped 7 production MCP server Actors in two weeks — here's what the docs don't tell you</title>
      <dc:creator>Noel Himer</dc:creator>
      <pubDate>Tue, 02 Jun 2026 17:41:16 +0000</pubDate>
      <link>https://dev.to/unbearablelabs/i-shipped-7-production-mcp-server-actors-in-two-weeks-heres-what-the-docs-dont-tell-you-3kgg</link>
      <guid>https://dev.to/unbearablelabs/i-shipped-7-production-mcp-server-actors-in-two-weeks-heres-what-the-docs-dont-tell-you-3kgg</guid>
      <description>&lt;h1&gt;
  
  
  I shipped 7 production MCP server Actors in two weeks — here's what the docs don't tell you
&lt;/h1&gt;

&lt;p&gt;The first Actor took most of a day. The seventh took under two hours. The delta wasn't clever abstractions or a better framework. It was the institutional knowledge that accumulates after you've hit every silent failure mode that Apify's documentation doesn't mention.&lt;/p&gt;

&lt;p&gt;This post covers the patterns that mattered. All of it works whether you buy anything at the end or not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack: what "MCP Actor on Apify" actually means
&lt;/h2&gt;

&lt;p&gt;Apify's Standby mode lets you run a persistent Python process that accepts HTTP connections. Combine that with &lt;a href="https://github.com/jlowin/fastmcp" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt; and you get an MCP server at a stable public URL, billed per tool call rather than per minute. The Standby URL format is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;username-hyphenated&amp;gt;--&amp;lt;actor-name&amp;gt;.apify.actor/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;unbearable-dev--docker-compose-audit.apify.actor/mcp&lt;/code&gt; is the live MCP endpoint for my docker-compose audit Actor. Any MCP client — Claude Desktop, Cursor, n8n, the Smithery gateway — can point at it directly.&lt;/p&gt;

&lt;p&gt;The basic scaffold is straightforward. The gotchas are not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 1: &lt;code&gt;webServerMcpPath&lt;/code&gt; — the silent routing killer
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;actor.json&lt;/code&gt; controls how Apify routes MCP traffic to your Standby process. The relevant field is &lt;code&gt;webServerMcpPath&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usesStandbyMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"webServerMcpPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/mcp"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this field is absent, misspelled, or set to anything other than the path your FastMCP server actually mounts at, you'll see the Actor status as "Running" in the Apify Console, the Standby URL will return a 404, and your MCP client will report a connection error. The Actor logs show a healthy process. Nothing in the logs points to the routing config.&lt;/p&gt;

&lt;p&gt;The field appears in one sentence in the Apify Standby documentation. It's easy to miss, easy to typo, and the failure mode is confusing because the Actor looks healthy from the outside.&lt;/p&gt;

&lt;p&gt;Fix: &lt;code&gt;webServerMcpPath&lt;/code&gt; must match the path argument you pass to FastMCP. If your server mounts at &lt;code&gt;/mcp&lt;/code&gt;, the field must be &lt;code&gt;/mcp&lt;/code&gt;. Lock them together and verify on first deploy with a curl to the Standby URL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 2: A 406 on &lt;code&gt;/mcp&lt;/code&gt; is a PASS, not a failure
&lt;/h2&gt;

&lt;p&gt;When you smoke-test a new Actor with a basic curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://unbearable-dev--docker-compose-audit.apify.actor/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back &lt;code&gt;406&lt;/code&gt;. First time I saw it I thought the routing was broken. It's not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;406 Not Acceptable&lt;/code&gt; means the server received the request, understood the path, and rejected it because the client sent no &lt;code&gt;Accept&lt;/code&gt; header specifying an MCP-compatible content type. The server is up. The MCP path is correct. The smoke test passes when you get a 406.&lt;/p&gt;

&lt;p&gt;A 404 means the path is wrong (see gotcha 1). A 503 means the Actor isn't running yet. A 406 is the health check you want.&lt;/p&gt;

&lt;p&gt;The correct smoke test for a raw Standby endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Expect 406 — this is the passing state&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; https://&amp;lt;actor-standby-url&amp;gt;/mcp | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;span class="c"&gt;# HTTP/2 406&lt;/span&gt;

&lt;span class="c"&gt;# Full MCP session test — expect {"result": {"tools": [...]}}&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://&amp;lt;actor-standby-url&amp;gt;/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second call is the real smoke test. Empty &lt;code&gt;tools&lt;/code&gt; array means your server is up but your tool registrations are not attached. Which brings us to gotcha 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 3: &lt;code&gt;get_server()&lt;/code&gt; scope and the zero-tool server
&lt;/h2&gt;

&lt;p&gt;FastMCP's pattern for Apify Standby looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_server&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-actor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;

&lt;span class="c1"&gt;# Don't do this:
&lt;/span&gt;&lt;span class="nd"&gt;@server.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you define tools outside the &lt;code&gt;get_server()&lt;/code&gt; function using the &lt;code&gt;@server.tool()&lt;/code&gt; decorator, they register against a different server instance than the one &lt;code&gt;get_server()&lt;/code&gt; returns. The Apify Standby framework calls &lt;code&gt;get_server()&lt;/code&gt; to get the server it will serve. Tools registered at module scope against a different instance are unreachable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tools/list&lt;/code&gt; returns an empty array. No error. No log entry. The Actor runs fine.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_server&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-actor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@server.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run my tool.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;do_the_work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything that needs to be reachable goes inside &lt;code&gt;get_server()&lt;/code&gt;. The returned instance is what gets served.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 4: PPE economics post-April 2026
&lt;/h2&gt;

&lt;p&gt;Apify retired rental pricing on April 1, 2026. Pay-per-event is now the only billing model for new Actors. The economics are better than they look.&lt;/p&gt;

&lt;p&gt;Every Apify user gets $5/month in free platform credits. At $0.02 per audit call, that's 250 free calls per user per month. The first dollar only flows after a user's free credits are exhausted. Solo developers and hobbyists may never pay — you're pricing for team and enterprise users whose credit buffers get consumed by actual volume.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pay_per_event.json&lt;/code&gt; schema ships in &lt;code&gt;.actor/pay_per_event.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"light-read"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"List / lookup call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charged for catalog, list, or lookup tool calls."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventPriceUsd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"heavy-call"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analysis / audit call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charged per primary tool call."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventPriceUsd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two tiers at a 4:1 ratio: cheap discovery, billable execution. Structure your tool catalog so &lt;code&gt;list_checks&lt;/code&gt;, &lt;code&gt;list_rules&lt;/code&gt;, and schema-query tools fire &lt;code&gt;light-read&lt;/code&gt; events; analysis and audit tools fire &lt;code&gt;heavy-call&lt;/code&gt; events.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Actor.charge()&lt;/code&gt; call goes before your logic, not after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit_dockerfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heavy-call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# charge first
&lt;/span&gt;    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# then do the work
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your logic raises an exception, the charge record still exists. Billing is accurate even on errors. If you charge after the work, a raised exception means an unbilled call — the platform economics break down at volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;pay_per_event.json&lt;/code&gt; placement gotcha:&lt;/strong&gt; the file goes in &lt;code&gt;.actor/pay_per_event.json&lt;/code&gt;, not the project root. If it's in the wrong location, &lt;code&gt;Actor.charge()&lt;/code&gt; executes without raising an error, but no billing event is recorded. No exception, no log entry, nothing on the Apify billing dashboard. The Actor runs correctly. You just aren't billing for it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;startedAt&lt;/code&gt; field in your PPE config must be at least 14 days from your push date. Setting it closer returns a &lt;code&gt;403 cannot-modify-actor-pricing-with-immediate-effect&lt;/code&gt; error. Set it 15–20 days out to be safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 5: The Dockerfile clobber guard
&lt;/h2&gt;

&lt;p&gt;AI coding agents — Cursor, Claude Code — occasionally write to &lt;code&gt;Dockerfile&lt;/code&gt; when they mean to write to a test fixture or a scratch file. &lt;code&gt;apify push&lt;/code&gt; uses whatever &lt;code&gt;Dockerfile&lt;/code&gt; is in the project root at push time. If the clobber happened before the push, your build uses the wrong base image, exits immediately, and the build logs point at dependency errors rather than the source problem.&lt;/p&gt;

&lt;p&gt;The fix: store a SHA-256 hash of &lt;code&gt;Dockerfile&lt;/code&gt; in &lt;code&gt;Dockerfile.sha256&lt;/code&gt; and verify it in a pre-commit hook.&lt;/p&gt;

&lt;p&gt;Generate the hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On Linux/Mac:&lt;/span&gt;
&lt;span class="nb"&gt;sha256sum &lt;/span&gt;Dockerfile | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dockerfile.sha256

&lt;span class="c"&gt;# On Windows (PowerShell):&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;Get-FileHash Dockerfile &lt;span class="nt"&gt;-Algorithm&lt;/span&gt; SHA256&lt;span class="o"&gt;)&lt;/span&gt;.Hash.ToLower&lt;span class="o"&gt;()&lt;/span&gt; | Set-Content Dockerfile.sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pre-commit hook (&lt;code&gt;.git/hooks/pre-commit&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nv"&gt;STORED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;Dockerfile.sha256&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ACTUAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sha256sum &lt;/span&gt;Dockerfile | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STORED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACTUAL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: Dockerfile has changed since Dockerfile.sha256 was generated."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  Stored:  &lt;/span&gt;&lt;span class="nv"&gt;$STORED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  Actual:  &lt;/span&gt;&lt;span class="nv"&gt;$ACTUAL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"If the change is intentional: sha256sum Dockerfile | awk '{print &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;1}' &amp;gt; Dockerfile.sha256"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an agent rewrites &lt;code&gt;Dockerfile&lt;/code&gt; accidentally, the hook catches it before the commit lands. Update &lt;code&gt;Dockerfile.sha256&lt;/code&gt; manually when you intentionally change the base image or build steps.&lt;/p&gt;

&lt;p&gt;The scaffold at &lt;code&gt;python-mcp-empty-plus&lt;/code&gt; ships with &lt;code&gt;Dockerfile.sha256&lt;/code&gt; already set to the correct hash for the default image (&lt;code&gt;apify/actor-python:3.14&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 6: Fix-snippet generation — what MCP clients can actually consume
&lt;/h2&gt;

&lt;p&gt;Audit-style Actors return a list of findings. The naive return format is a wall of text or a raw diff. Neither is useful in an agentic workflow.&lt;/p&gt;

&lt;p&gt;The pattern that works: a structured response envelope with a machine-readable &lt;code&gt;fixes&lt;/code&gt; array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypedDict&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Fix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# "error" | "warning" | "info"
&lt;/span&gt;    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;fix_snippet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;       &lt;span class="c1"&gt;# the corrected content, ready to write
&lt;/span&gt;    &lt;span class="n"&gt;line_range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# [start, end] line numbers
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuditResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;issue_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;fixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fix_snippet&lt;/code&gt; field carries the corrected content, not a description of what to change. An MCP client driving Claude Desktop can write the fix snippet directly to the file. A diff description requires the agent to derive the edit — an extra round trip and a source of hallucination.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;RULE_MAP&lt;/code&gt; pattern separates rule definitions from audit logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;RULE_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DF-001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use specific base image tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix_template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FROM {image}:{pinned_tag}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules as data, not conditionals. Adding a new check is one dict entry. The audit loop iterates &lt;code&gt;RULE_MAP&lt;/code&gt; and populates &lt;code&gt;fixes&lt;/code&gt;. Clean separation means you can test each rule in isolation without running a full audit.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the 7 production Actors look like
&lt;/h2&gt;

&lt;p&gt;The Actors live on the Apify Store under &lt;code&gt;unbearable-dev&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker-compose-audit&lt;/code&gt; — 25 checks across 9 categories (security, networking, resource limits, image policy, restart policy, secrets management, health checks, logging, compose-file best practices)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dockerfile-audit&lt;/code&gt; — 19 checks across 5 categories (base image, build hygiene, layer efficiency, secrets, user privilege)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;github-actions-audit&lt;/code&gt; — 21 checks across 6 categories (action pinning, permission scoping, secret hygiene, event trigger misconfigurations, dependency audit, SLSA provenance)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;k8s-manifest-audit&lt;/code&gt; — 63 checks across 7 categories (Deployment, Service, RBAC, NetworkPolicy, PodSecurity, and more)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iac-audit-pack&lt;/code&gt; — the composite: all 4 domains, 131 checks, 43 tools in one Actor&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hu-postcode-validator&lt;/code&gt; — 5 utility tools backed by the Hungarian postal code dataset&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;szamlazz-mcp&lt;/code&gt; — 8 tools wrapping the Számlázz.hu invoicing API, BYO-API-key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of them are also wired into a self-hosted HTTP multiplexer on the Pi (&lt;code&gt;unbearable-mcp&lt;/code&gt; at port 8700) that exposes them behind a single MCP endpoint. The Smithery catalog lists them individually with a gateway parameter that routes to the right Actor.&lt;/p&gt;




&lt;h2&gt;
  
  
  The boilerplate I ended up with
&lt;/h2&gt;

&lt;p&gt;After Actor 4, the setup was stable enough to extract. The scaffold (&lt;code&gt;python-mcp-empty-plus&lt;/code&gt;) is on GitHub — it's the structure without the explanations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;actor.json&lt;/code&gt; with &lt;code&gt;webServerMcpPath&lt;/code&gt; already set&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Dockerfile&lt;/code&gt; pinned to &lt;code&gt;apify/actor-python:3.14&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Dockerfile.sha256&lt;/code&gt; for the clobber guard&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.actor/pay_per_event.json&lt;/code&gt; with the two-tier template and the PPE gotchas documented inline&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; with &lt;code&gt;fastmcp&lt;/code&gt; and &lt;code&gt;apify-client&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/main.py&lt;/code&gt; with the &lt;code&gt;get_server()&lt;/code&gt; pattern and a stub tool&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/smoke.sh&lt;/code&gt; — the three-tier smoke test (unit → &lt;code&gt;python -m&lt;/code&gt; → curl &lt;code&gt;/mcp&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Pre-commit hook wired in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 8-doc pack is the explanations. It covers every pattern in this post in full, with the &lt;code&gt;markdown-linter-mcp&lt;/code&gt; example Actor worked through from scaffold to Smithery listing. That Actor is ~200 lines, fully functional, 5 tools, PPE configured. The source ships with the pack.&lt;/p&gt;

&lt;p&gt;The docs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Quickstart — zero to Standby endpoint in one session&lt;/li&gt;
&lt;li&gt;Check-catalog pattern — &lt;code&gt;list_checks&lt;/code&gt; tool structure and how it shapes PPE billing&lt;/li&gt;
&lt;li&gt;Fix-snippet generation — response envelope, &lt;code&gt;RULE_MAP&lt;/code&gt;, why raw diffs break MCP clients&lt;/li&gt;
&lt;li&gt;PPE economics post-April 2026 — two-tier design, &lt;code&gt;Actor.charge()&lt;/code&gt; placement, break-even math&lt;/li&gt;
&lt;li&gt;Standby URL and cold-start behavior — URL anatomy, Bearer auth, session tracking middleware&lt;/li&gt;
&lt;li&gt;Smoke testing — three-tier harness and what each tier catches&lt;/li&gt;
&lt;li&gt;Dockerfile clobber guard — the SHA-256 approach and the pre-commit hook&lt;/li&gt;
&lt;li&gt;Troubleshooting — 15 failure modes with exact error messages and fixes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you've read this far and the patterns above saved you time, the pack is €49 at &lt;a href="https://unbearable0.gumroad.com/l/mcp-server-boilerplate-pack?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=boilerplate-pack" rel="noopener noreferrer"&gt;unbearable0.gumroad.com/l/mcp-server-boilerplate-pack&lt;/a&gt;. One payment, immediate download, 14-day refund.&lt;/p&gt;

&lt;p&gt;If you're not at the "buy a doc pack" stage yet, the free scaffold is at &lt;a href="https://github.com/UnbearableDev/python-mcp-empty-plus" rel="noopener noreferrer"&gt;github.com/UnbearableDev/python-mcp-empty-plus&lt;/a&gt;. It gets you past the blank-slate problem. The gotchas above are free either way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Noel @ &lt;a href="https://unbearabletechtips.beehiiv.com?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=boilerplate-pack" rel="noopener noreferrer"&gt;Unbearable Labs&lt;/a&gt; — practical homelab and agent ops. The newsletter is weekly, no filler.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>apify</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>If you use Trivy or KICS in CI, read this</title>
      <dc:creator>Noel Himer</dc:creator>
      <pubDate>Thu, 28 May 2026 17:44:24 +0000</pubDate>
      <link>https://dev.to/unbearablelabs/if-you-use-trivy-or-kics-in-ci-read-this-4d91</link>
      <guid>https://dev.to/unbearablelabs/if-you-use-trivy-or-kics-in-ci-read-this-4d91</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;75 of 76 trivy-action tags hijacked in five days. The pattern, three checks, and what to automate.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Hey —&lt;/p&gt;

&lt;p&gt;Between March 19 and March 24, 2026, the "TeamPCP" actor force-pushed mutable tags on three popular security-tool repos. &lt;strong&gt;75 of 76 &lt;code&gt;trivy-action&lt;/code&gt; tags&lt;/strong&gt; plus 7 &lt;code&gt;setup-trivy&lt;/code&gt; tags went first. Four days later, &lt;strong&gt;all 91 Checkmarx KICS action tags&lt;/strong&gt; were repointed. The same group landed malicious LiteLLM builds on PyPI on the 24th. Every CI pipeline pinned to &lt;code&gt;@v0&lt;/code&gt;, &lt;code&gt;@main&lt;/code&gt;, or &lt;code&gt;@latest&lt;/code&gt; on those actions ran attacker code on its next build.&lt;/p&gt;

&lt;p&gt;The injected payload was not subtle: it scraped the hosted GitHub runner's process memory for variables marked &lt;code&gt;isSecret: true&lt;/code&gt;, swept the filesystem for SSH keys and cloud credentials, encrypted everything with AES-256-CBC + RSA-4096, and exfiltrated it.&lt;/p&gt;

&lt;p&gt;If you used Trivy or KICS in CI without a SHA pin, &lt;strong&gt;assume those secrets are gone.&lt;/strong&gt; Rotate, then come back to this email.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism in one paragraph
&lt;/h2&gt;

&lt;p&gt;A GitHub Actions reference like &lt;code&gt;uses: aquasecurity/trivy-action@v0&lt;/code&gt; is just a &lt;strong&gt;pointer&lt;/strong&gt; to a git ref. Anyone with push to that repo — including an attacker who steals a maintainer token — can &lt;code&gt;git tag -f v0 &amp;lt;attacker-commit&amp;gt; &amp;amp;&amp;amp; git push --force&lt;/code&gt; and now every pipeline pinned to &lt;code&gt;v0&lt;/code&gt; builds the attacker's code. Branches are worse. Even semver-style tags like &lt;code&gt;@v2&lt;/code&gt; are mutable. The only ref form that is cryptographically immutable is the &lt;strong&gt;full 40-character commit SHA&lt;/strong&gt;. From the Puma Security writeup:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A commit SHA is a cryptographic hash of the repository state at that point in time. It cannot be moved or reassigned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the whole defense. The rest is logistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three checks you can run today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Grep your workflows for unpinned refs.&lt;/strong&gt; Five minutes, no tooling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rEn&lt;/span&gt; &lt;span class="s1"&gt;'uses: [^@]+@(main|master|v[0-9]+(\.[0-9]+)?)'&lt;/span&gt; .github/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything that prints is a candidate for repinning. If you have third-party actions there (anything not &lt;code&gt;actions/*&lt;/code&gt; or &lt;code&gt;github/*&lt;/code&gt;), prioritize those first — that's the TeamPCP exposure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Replace high-risk pins with SHA + tag comment.&lt;/strong&gt; The standard form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@&amp;lt;40-char-sha&amp;gt;&lt;/span&gt;  &lt;span class="c1"&gt;# v0.x.y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub's UI shows the SHA on every release page. Dependabot understands this form and proposes SHA updates with the matching tag comment. You give up zero ergonomics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Audit your top-level &lt;code&gt;permissions:&lt;/code&gt; block.&lt;/strong&gt; A workflow without an explicit &lt;code&gt;permissions:&lt;/code&gt; key inherits &lt;code&gt;contents: write&lt;/code&gt; by default on most repos. Add this near the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…then grant per-job writes only where needed. If the TeamPCP payload had landed on a repo using &lt;code&gt;permissions: read-all&lt;/code&gt;, the blast radius would have been the runner secrets — not also the ability to push commits back.&lt;/p&gt;

&lt;p&gt;These three checks take ten minutes total on a single workflow. Most of you have multiple workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I'm wiring it for ongoing use
&lt;/h2&gt;

&lt;p&gt;I shipped &lt;code&gt;github-actions-audit&lt;/code&gt; on Apify Store earlier this month — 13 checks for the published CI attack surface. After TeamPCP I'm extending it with an 8-check &lt;code&gt;supply_chain_advanced&lt;/code&gt; category (&lt;code&gt;GHA-201&lt;/code&gt; through &lt;code&gt;GHA-208&lt;/code&gt;) that catches the specific patterns the attackers exploited: mutable tag refs, &lt;code&gt;pull_request_target&lt;/code&gt; + checkout-by-PR-sha, script injection via &lt;code&gt;${{ github.event.* }}&lt;/code&gt;, untrusted owners, &lt;code&gt;permissions: write-all&lt;/code&gt; defaults.&lt;/p&gt;

&lt;p&gt;The MCP version lets Claude or Cursor agents run the audit against a workflow YAML on demand — paste a file, get back severity, line numbers, and a copy-paste fix snippet. Pricing is unchanged: $0.02 per audit. The extension lands in the same Actor — no migration.&lt;/p&gt;

&lt;p&gt;If you want the CLI-shaped version of the same defense, &lt;strong&gt;zizmor&lt;/strong&gt; by William Woodruff is the open-source linter that pioneered most of these checks; it's how I cross-checked our findings during development. I'd run both: zizmor in pre-commit, the MCP server in agentic flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Find every unpinned third-party action across your repos&lt;/span&gt;
gh repo list &lt;span class="nt"&gt;--limit&lt;/span&gt; 1000 &lt;span class="nt"&gt;--json&lt;/span&gt; nameWithOwner &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'.[].nameWithOwner'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;repo&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== &lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    gh api &lt;span class="s2"&gt;"repos/&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;/contents/.github/workflows"&lt;/span&gt; &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.[].path'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;wf&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        &lt;/span&gt;gh api &lt;span class="s2"&gt;"repos/&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;/contents/&lt;/span&gt;&lt;span class="nv"&gt;$wf&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.content'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-En&lt;/span&gt; &lt;span class="s1"&gt;'uses: [^@]+@(main|master|v[0-9]+(\.[0-9]+)?)'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s2"&gt;"s|^|  &lt;/span&gt;&lt;span class="nv"&gt;$wf&lt;/span&gt;&lt;span class="s2"&gt;:|"&lt;/span&gt;
      &lt;span class="k"&gt;done
  done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's three hours of grunt work compressed into one command. Run it, fix what falls out, sleep better.&lt;/p&gt;

&lt;p&gt;For the MCP-native flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"github-actions-audit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://unbearable-dev--github-actions-audit.apify.actor/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer &amp;lt;YOUR_APIFY_TOKEN&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Claude Desktop, paste a workflow YAML, ask for an audit. The new GHA-201..208 checks are coming in the next push.&lt;/p&gt;

&lt;h2&gt;
  
  
  This week's reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pumasecurity.io/resources/blog/teampcp-github-actions-supply-chain/" rel="noopener noreferrer"&gt;Puma Security: TeamPCP GitHub Actions supply chain&lt;/a&gt;&lt;/strong&gt; — the technical writeup with timeline + IoCs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/woodruffw/zizmor" rel="noopener noreferrer"&gt;zizmor on GitHub&lt;/a&gt;&lt;/strong&gt; — the linter the audit shop's &lt;code&gt;supply_chain_advanced&lt;/code&gt; checks are modeled on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/step-security/harden-runner" rel="noopener noreferrer"&gt;StepSecurity Harden Runner&lt;/a&gt;&lt;/strong&gt; — egress filtering on the GitHub-hosted runner; catches the AES/RSA exfil leg of TeamPCP-class attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/unbearable_dev/github-actions-audit" rel="noopener noreferrer"&gt;apify.com/unbearable_dev/github-actions-audit&lt;/a&gt;&lt;/strong&gt; — the Actor itself, $0.02 per audit&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;built by Noel @ &lt;a href="https://unbearabletechtips.beehiiv.com" rel="noopener noreferrer"&gt;Unbearable TechTips&lt;/a&gt; — practical homelab + agent ops. Reply to this email — I read every one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="mailto:unbearabledev@gmail.com?subject=Sponsorship%20inquiry"&gt;Sponsor this newsletter&lt;/a&gt; · &lt;a href="https://github.com/UnbearableDev" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.youtube.com/@UnbearableTechTips" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>devops</category>
      <category>supplychain</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
