<?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: Scott Glover</title>
    <description>The latest articles on DEV Community by Scott Glover (@scottgl9).</description>
    <link>https://dev.to/scottgl9</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3933963%2F299aab4f-add6-455f-bada-916634c34794.jpeg</url>
      <title>DEV Community: Scott Glover</title>
      <link>https://dev.to/scottgl9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scottgl9"/>
    <language>en</language>
    <item>
      <title>I built skelm because n8n, OpenClaw, and Hermes didn't fit my use case</title>
      <dc:creator>Scott Glover</dc:creator>
      <pubDate>Fri, 15 May 2026 22:35:10 +0000</pubDate>
      <link>https://dev.to/scottgl9/i-built-skelm-because-n8n-openclaw-and-hermes-didnt-fit-my-use-case-25ob</link>
      <guid>https://dev.to/scottgl9/i-built-skelm-because-n8n-openclaw-and-hermes-didnt-fit-my-use-case-25ob</guid>
      <description>&lt;p&gt;I needed to run AI agent workflows locally, in TypeScript, with real permission&lt;br&gt;
enforcement. I spent time with the obvious options — n8n, OpenClaw, Hermes —&lt;br&gt;
and none of them quite fit. So I built &lt;a href="https://skelm.dev" rel="noopener noreferrer"&gt;skelm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is about what specifically didn't fit, what skelm does differently,&lt;br&gt;
and one concrete example of why the difference matters.&lt;/p&gt;


&lt;h2&gt;
  
  
  What the existing tools get wrong (for my use case)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  n8n
&lt;/h3&gt;

&lt;p&gt;n8n is a genuinely great tool. But it's built around visual flows and a&lt;br&gt;
node-graph model. I want to write real &lt;code&gt;.ts&lt;/code&gt; modules, use the full TypeScript&lt;br&gt;
type system, run &lt;code&gt;vitest&lt;/code&gt;, and check my workflows into git like any other&lt;br&gt;
code. I don't want a JSON config I can't diff cleanly or a canvas I need&lt;br&gt;
a browser to edit.&lt;/p&gt;
&lt;h3&gt;
  
  
  OpenClaw
&lt;/h3&gt;

&lt;p&gt;OpenClaw is excellent for personal assistant use cases — chat routing,&lt;br&gt;
multi-channel, agent conversations. But it's fundamentally a &lt;em&gt;messaging&lt;/em&gt;&lt;br&gt;
framework. I needed &lt;em&gt;pipeline&lt;/em&gt; primitives: &lt;code&gt;branch&lt;/code&gt;, &lt;code&gt;parallel&lt;/code&gt;, &lt;code&gt;forEach&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;loop&lt;/code&gt;, &lt;code&gt;wait&lt;/code&gt;. Composable, testable, deterministic. OpenClaw isn't built&lt;br&gt;
for that and doesn't pretend to be.&lt;/p&gt;
&lt;h3&gt;
  
  
  Hermes and most LLM orchestration frameworks
&lt;/h3&gt;

&lt;p&gt;The permission model is advisory. You tell the model what it &lt;em&gt;should&lt;/em&gt; do&lt;br&gt;
in the system prompt. The model tries to comply. This works until it doesn't&lt;br&gt;
— a new tool capability ships, the model decides the task requires it anyway,&lt;br&gt;
or a crafted input nudges it past the instruction. Advisory permissions are&lt;br&gt;
not a security boundary. They're a politeness request.&lt;/p&gt;


&lt;h2&gt;
  
  
  What skelm does differently
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Real TypeScript, not a DSL
&lt;/h3&gt;

&lt;p&gt;Workflows are &lt;code&gt;.ts&lt;/code&gt; files. No YAML, no JSON config, no visual canvas. The&lt;br&gt;
full type system, full test runner, full IDE support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triage-incident&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;incidentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;severity-gate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search-issues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchGitHub&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create-channel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createSlackChannel&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notify-oncall&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;notifyOncall&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;acknowledge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sendAck&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root-cause&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;codex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;buildRcaPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;fsRead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./runbooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;fsWrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="na"&gt;networkEgress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real, runnable workflow. &lt;code&gt;skelm run triage-incident.workflow.ts&lt;br&gt;
--input '{"incidentId":"INC-001","severity":"critical"}'&lt;/code&gt;. No gateway&lt;br&gt;
required for local runs. No browser.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Structural permissions, not advisory ones
&lt;/h3&gt;

&lt;p&gt;This is the core design decision that everything else follows from.&lt;/p&gt;

&lt;p&gt;In skelm, permissions are enforced at the &lt;em&gt;boundary&lt;/em&gt; before the backend is&lt;br&gt;
called — not in the context window. Here's what that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fix-bug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;codex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`Fix: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fsRead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;fsWrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;           &lt;span class="c1"&gt;// only src — tests and config stay read-only&lt;/span&gt;
    &lt;span class="na"&gt;allowedExecutables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;networkEgress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;allowedMcpServers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;allowedSkills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These aren't hints. They're enforced at three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-run mapper&lt;/strong&gt; — before any SDK call, skelm translates the policy into&lt;br&gt;
backend-specific options and refuses unsafe combinations. &lt;code&gt;fsWrite: ['*']&lt;/code&gt;&lt;br&gt;
with an approval policy set throws &lt;code&gt;CodexPermissionError&lt;/code&gt; immediately.&lt;br&gt;
Skelm refuses to silently escalate to &lt;code&gt;danger-full-access&lt;/code&gt; sandbox mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend in-process enforcement&lt;/strong&gt; — Codex's sandbox, Pi's permission&lt;br&gt;
enforcer, and skelm's own &lt;code&gt;@skelm/agent&lt;/code&gt; backend all enforce natively&lt;br&gt;
in-process. Skelm's job is to ensure the translation from declared&lt;br&gt;
permissions to backend options is complete and doesn't widen anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gateway egress proxy&lt;/strong&gt; — the gateway runs an embedded CONNECT proxy.&lt;br&gt;
&lt;code&gt;HTTP_PROXY&lt;/code&gt; is merged into subprocess environments so outbound TCP is&lt;br&gt;
intercepted. &lt;code&gt;allowHosts&lt;/code&gt; is enforced at the TCP level, not in the model's&lt;br&gt;
context window.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. A concrete example of why this matters
&lt;/h3&gt;

&lt;p&gt;When I wired up the Codex backend I set &lt;code&gt;networkEgress: 'deny'&lt;/code&gt; and mapped&lt;br&gt;
it to &lt;code&gt;networkAccessEnabled: false&lt;/code&gt;. That blocks sandbox-shell network&lt;br&gt;
calls — &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;But Codex also has a built-in &lt;code&gt;web_search&lt;/code&gt; tool that runs &lt;em&gt;inside the Codex&lt;br&gt;
process&lt;/em&gt;, not through the sandbox shell. &lt;code&gt;networkAccessEnabled: false&lt;/code&gt; does&lt;br&gt;
not disable it. My deny was covering one surface and missing another.&lt;/p&gt;

&lt;p&gt;Here's the fix in the permission mapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Codex has two distinct network surfaces:&lt;/span&gt;
&lt;span class="c1"&gt;//   1. networkAccessEnabled — sandbox-shell egress (curl, wget, etc.)&lt;/span&gt;
&lt;span class="c1"&gt;//   2. webSearchMode        — model's built-in tool, runs in-process,&lt;/span&gt;
&lt;span class="c1"&gt;//                            NOT interceptable by the gateway proxy&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;networkAccessEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;networkEgress&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// web_search enabled ONLY on explicit blanket 'allow'.&lt;/span&gt;
&lt;span class="c1"&gt;// { allowHosts: ['api.example.com'] } also disables it —&lt;/span&gt;
&lt;span class="c1"&gt;// the proxy can enforce allowHosts for shell TCP, but not in-process search.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webSearchAllowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;networkEgress&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webSearchEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;webSearchAllowed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webSearchMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebSearchMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;webSearchAllowed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is exactly the kind of gap advisory permissions can't cover. A system&lt;br&gt;
prompt that says "don't access the network" doesn't know &lt;code&gt;web_search&lt;/code&gt; exists.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Multi-backend, pluggable, not locked in
&lt;/h3&gt;

&lt;p&gt;I wanted to swap backends without rewriting workflows. skelm supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@skelm/agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;First-party, any OpenAI-compatible endpoint, in-process enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@skelm/codex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI Codex via &lt;code&gt;@openai/codex-sdk&lt;/code&gt; — sandbox-aware, MCP, streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@skelm/pi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pi coding agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@skelm/opencode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opencode coding agent (native + ACP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@skelm/vercel-ai&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vercel AI SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in ACP&lt;/td&gt;
&lt;td&gt;Copilot, Claude Code, Gemini CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same &lt;code&gt;agent()&lt;/code&gt; step works across all of them. Swap &lt;code&gt;backend: 'pi'&lt;/code&gt; for&lt;br&gt;
&lt;code&gt;backend: 'codex'&lt;/code&gt; and the permission model, skill injection, and streaming&lt;br&gt;
all carry through.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Human-in-the-loop as a first-class primitive
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;wait()&lt;/code&gt; pauses a run until a caller resumes it via HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;human-review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Review this expense and approve or reject it.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approve&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="na"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Resume from anywhere&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:14738/runs/&amp;lt;runId&amp;gt;/resume &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"output":{"decision":"approve","comments":"Looks good"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a webhook integration, not a plugin. A first-class step kind.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Tamper-evident audit
&lt;/h3&gt;

&lt;p&gt;Every permission decision, tool call, secret access, and approval is written&lt;br&gt;
to a hash-chained audit journal. You can verify the chain hasn't been altered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skelm audit query &lt;span class="nt"&gt;--verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for compliance use cases where you need to prove what the agent&lt;br&gt;
was &lt;em&gt;actually&lt;/em&gt; permitted to do, not just what it was told.&lt;/p&gt;




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

&lt;p&gt;It's not a finished product. Pre-v1, APIs unstable, some rough edges. The&lt;br&gt;
permission model is solid and the core step primitives work. The ecosystem&lt;br&gt;
around it — integrations, docs, tooling — is still growing.&lt;/p&gt;

&lt;p&gt;It's also not trying to replace n8n for visual workflow automation, or&lt;br&gt;
OpenClaw for conversational agents. It occupies a different space: you want&lt;br&gt;
to write code, you care about what your agents are actually &lt;em&gt;permitted&lt;/em&gt; to&lt;br&gt;
do, and you want that enforced structurally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; skelm
skelm init my-project &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-project &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install
&lt;/span&gt;skelm run workflows/hello.workflow.ts &lt;span class="nt"&gt;--input&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"world"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full docs and source: &lt;a href="https://github.com/scottgl9/skelm" rel="noopener noreferrer"&gt;github.com/scottgl9/skelm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've hit the same wall with n8n, OpenClaw, Hermes, or any other&lt;br&gt;
orchestration framework — I'd genuinely like to hear what the gap was for&lt;br&gt;
you. Happy to answer questions about the permission model or any of the&lt;br&gt;
backend integrations in the comments.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
