Skip to content

Instantly share code, notes, and snippets.

@Patrick-Erichsen
Last active June 5, 2026 19:42
Show Gist options
  • Select an option

  • Save Patrick-Erichsen/7d6b3cae5e37e80e2c84fab0409f2cfd to your computer and use it in GitHub Desktop.

Select an option

Save Patrick-Erichsen/7d6b3cae5e37e80e2c84fab0409f2cfd to your computer and use it in GitHub Desktop.
ClawHub scanner-triggered ban policy flow #pagedrop
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ClawHub Scanner-Triggered Ban Flow</title>
<style>
:root {
--bg: #f8faf8;
--paper: #ffffff;
--ink: #18211d;
--muted: #5d6b63;
--line: #d8e0da;
--red: #b42318;
--red-soft: #fff1f0;
--amber: #a15c07;
--amber-soft: #fff8e8;
--green: #16703f;
--green-soft: #eefaf2;
--blue: #1b5d8f;
--blue-soft: #edf6ff;
--code: #0f172a;
--shadow: 0 14px 32px rgba(30, 41, 59, 0.08);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 15px/1.55 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
}
main {
max-width: 1180px;
margin: 0 auto;
padding: 34px 20px 54px;
}
header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 22px;
align-items: end;
border-bottom: 1px solid var(--line);
padding-bottom: 22px;
margin-bottom: 24px;
}
h1,
h2,
h3,
p {
margin: 0;
}
h1 {
font-size: clamp(30px, 5vw, 54px);
line-height: 0.98;
letter-spacing: 0;
}
h2 {
font-size: 22px;
line-height: 1.15;
margin-bottom: 12px;
}
h3 {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 10px;
}
.subtitle {
max-width: 760px;
margin-top: 14px;
color: var(--muted);
font-size: 17px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid var(--line);
background: var(--paper);
border-radius: 999px;
padding: 8px 12px;
color: var(--muted);
white-space: nowrap;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 18px;
}
.current {
border-top: 5px solid var(--red);
}
.proposed {
border-top: 5px solid var(--green);
}
.callout {
margin: 20px 0;
border: 1px solid var(--line);
background: var(--paper);
border-radius: 8px;
padding: 16px 18px;
display: grid;
grid-template-columns: 180px minmax(0, 1fr);
gap: 18px;
box-shadow: var(--shadow);
}
.callout strong {
display: block;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--green);
}
.flow {
display: grid;
gap: 10px;
}
.step {
position: relative;
border: 1px solid var(--line);
background: #fbfdfb;
border-radius: 8px;
padding: 12px 12px 12px 42px;
min-height: 64px;
}
.step::before {
content: attr(data-step);
position: absolute;
left: 12px;
top: 13px;
width: 20px;
height: 20px;
border-radius: 999px;
display: grid;
place-items: center;
font-size: 12px;
font-weight: 800;
color: #fff;
background: var(--blue);
}
.current .step.hot::before,
.current .step.danger::before {
background: var(--red);
}
.proposed .step.safe::before {
background: var(--green);
}
.step + .step::after {
content: "";
position: absolute;
left: 21px;
top: -11px;
width: 2px;
height: 10px;
background: var(--line);
}
.step b {
display: block;
margin-bottom: 2px;
}
.step span {
display: block;
color: var(--muted);
font-size: 14px;
}
.pill {
display: inline-block;
margin-top: 8px;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.pill.red {
color: var(--red);
background: var(--red-soft);
}
.pill.amber {
color: var(--amber);
background: var(--amber-soft);
}
.pill.green {
color: var(--green);
background: var(--green-soft);
}
.pill.blue {
color: var(--blue);
background: var(--blue-soft);
}
.split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-top: 18px;
}
table {
width: 100%;
border-collapse: collapse;
background: var(--paper);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow);
}
th,
td {
text-align: left;
vertical-align: top;
padding: 11px 12px;
border-bottom: 1px solid var(--line);
}
th {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
background: #f1f6f2;
}
tr:last-child td {
border-bottom: 0;
}
code {
color: var(--code);
background: #eef2f6;
border: 1px solid #dde5ec;
border-radius: 5px;
padding: 1px 5px;
font-size: 12px;
}
.repo {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.repo-item {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper);
padding: 12px;
}
.repo-item b {
display: block;
margin-bottom: 6px;
}
.repo-item p {
color: var(--muted);
font-size: 13px;
}
.mini-map {
width: 100%;
min-height: 210px;
border: 1px solid var(--line);
border-radius: 8px;
background: linear-gradient(180deg, #ffffff, #f6faf7);
margin: 12px 0 4px;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 8px;
color: var(--muted);
font-size: 13px;
margin-top: 8px;
}
.dot {
width: 9px;
height: 9px;
display: inline-block;
border-radius: 99px;
margin-right: 5px;
}
.dot.red {
background: var(--red);
}
.dot.green {
background: var(--green);
}
.dot.blue {
background: var(--blue);
}
.dot.amber {
background: var(--amber);
}
footer {
margin-top: 22px;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 860px) {
header,
.grid,
.split,
.repo,
.callout {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main>
<header>
<div>
<h1>Move scanner enforcement from account bans to blocked attempts.</h1>
<p class="subtitle">
Repo read: a ClawScan malicious skill result hides the skill, schedules
<code>users.autobanMalwareAuthorInternal</code>, then bans the publisher and hides their
other listings. The refined direction: block the malicious publish attempt, email local
scan remediation guidance, and ban only after repeated malicious outcomes.
</p>
</div>
<div class="badge">Planning artifact - no repo changes</div>
</header>
<section class="callout">
<strong>Verdict</strong>
<p>
We can make the system much less punitive without losing safety by splitting
<b>publish-attempt risk</b> from <b>account risk</b>. A first malicious finding should block
that submitted version and teach the publisher how to fix it locally. Appeals stay
account-level only.
</p>
</section>
<section class="grid" aria-label="Current and proposed flow diagrams">
<article class="card current">
<h2>Current Behavior</h2>
<svg class="mini-map" viewBox="0 0 640 250" role="img" aria-label="Current flow">
<defs>
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L10,4 L0,8 Z" fill="#b42318" />
</marker>
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L10,4 L0,8 Z" fill="#1b5d8f" />
</marker>
</defs>
<rect x="24" y="32" width="120" height="56" rx="8" fill="#edf6ff" stroke="#a6c9e8" />
<text x="84" y="58" text-anchor="middle" font-size="14" font-weight="700">Publish</text>
<text x="84" y="76" text-anchor="middle" font-size="12">new version</text>
<line x1="144" y1="60" x2="228" y2="60" stroke="#1b5d8f" stroke-width="2" marker-end="url(#arrow-blue)" />
<rect x="228" y="32" width="140" height="56" rx="8" fill="#fff8e8" stroke="#e9c783" />
<text x="298" y="57" text-anchor="middle" font-size="14" font-weight="700">Promote</text>
<text x="298" y="76" text-anchor="middle" font-size="12">latest pointer</text>
<line x1="368" y1="60" x2="452" y2="60" stroke="#1b5d8f" stroke-width="2" marker-end="url(#arrow-blue)" />
<rect x="452" y="32" width="140" height="56" rx="8" fill="#fff8e8" stroke="#e9c783" />
<text x="522" y="57" text-anchor="middle" font-size="14" font-weight="700">Scan async</text>
<text x="522" y="76" text-anchor="middle" font-size="12">VT + ClawScan</text>
<line x1="522" y1="88" x2="522" y2="128" stroke="#b42318" stroke-width="2" marker-end="url(#arrow-red)" />
<rect x="452" y="128" width="140" height="56" rx="8" fill="#fff1f0" stroke="#e7a6a0" />
<text x="522" y="152" text-anchor="middle" font-size="14" font-weight="700">Malicious</text>
<text x="522" y="171" text-anchor="middle" font-size="12">hidden skill</text>
<line x1="452" y1="156" x2="366" y2="156" stroke="#b42318" stroke-width="2" marker-end="url(#arrow-red)" />
<rect x="226" y="128" width="140" height="56" rx="8" fill="#fff1f0" stroke="#e7a6a0" />
<text x="296" y="152" text-anchor="middle" font-size="14" font-weight="700">Autoban</text>
<text x="296" y="171" text-anchor="middle" font-size="12">owner account</text>
<line x1="226" y1="156" x2="144" y2="156" stroke="#b42318" stroke-width="2" marker-end="url(#arrow-red)" />
<rect x="24" y="128" width="120" height="56" rx="8" fill="#fff1f0" stroke="#e7a6a0" />
<text x="84" y="152" text-anchor="middle" font-size="14" font-weight="700">Nuke blast</text>
<text x="84" y="171" text-anchor="middle" font-size="12">tokens + all listings</text>
</svg>
<div class="flow">
<div class="step" data-step="1">
<b>Publish writes the version first.</b>
<span>
<code>publishVersionForUser</code> inserts the version, then schedules VT, ClawScan,
and dependency scans.
</span>
<span class="pill amber">async scan after write</span>
</div>
<div class="step" data-step="2">
<b>If semver is newer, it becomes latest immediately.</b>
<span>
<code>insertVersion</code> updates <code>latestVersionId</code> and the
<code>latest</code> tag before final scanner outcome.
</span>
<span class="pill amber">previous clean version can be displaced</span>
</div>
<div class="step hot" data-step="3">
<b>ClawScan malicious means skill hidden.</b>
<span>
Structured moderation sets <code>moderationVerdict="malicious"</code> and
<code>moderationStatus="hidden"</code>.
</span>
<span class="pill red">artifact blocked</span>
</div>
<div class="step danger" data-step="4">
<b>Then it schedules account autoban.</b>
<span>
<code>scheduleClawScanAutobanForMalware</code> calls
<code>users.autobanMalwareAuthorInternal</code>.
</span>
<span class="pill red">artifact risk becomes account risk</span>
</div>
<div class="step danger" data-step="5">
<b>Autoban hides owned content and blocks login.</b>
<span>
The mutation soft-deletes owned skills, revokes API tokens, applies package bans,
clears telemetry, and sets <code>user.deletedAt</code>.
</span>
<span class="pill red">full-account recovery problem</span>
</div>
</div>
</article>
<article class="card proposed">
<h2>Proposed Behavior</h2>
<svg class="mini-map" viewBox="0 0 640 250" role="img" aria-label="Proposed flow">
<defs>
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L10,4 L0,8 Z" fill="#16703f" />
</marker>
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L10,4 L0,8 Z" fill="#a15c07" />
</marker>
</defs>
<rect x="24" y="32" width="120" height="56" rx="8" fill="#edf6ff" stroke="#a6c9e8" />
<text x="84" y="57" text-anchor="middle" font-size="14" font-weight="700">Publish</text>
<text x="84" y="76" text-anchor="middle" font-size="12">candidate version</text>
<line x1="144" y1="60" x2="228" y2="60" stroke="#16703f" stroke-width="2" marker-end="url(#arrow-green)" />
<rect x="228" y="32" width="140" height="56" rx="8" fill="#eefaf2" stroke="#a9dabd" />
<text x="298" y="57" text-anchor="middle" font-size="14" font-weight="700">Hold until clean</text>
<text x="298" y="76" text-anchor="middle" font-size="12">or keep prior latest</text>
<line x1="368" y1="60" x2="452" y2="60" stroke="#16703f" stroke-width="2" marker-end="url(#arrow-green)" />
<rect x="452" y="32" width="140" height="56" rx="8" fill="#fff8e8" stroke="#e9c783" />
<text x="522" y="57" text-anchor="middle" font-size="14" font-weight="700">Scan async</text>
<text x="522" y="76" text-anchor="middle" font-size="12">final verdict</text>
<line x1="522" y1="88" x2="522" y2="128" stroke="#a15c07" stroke-width="2" marker-end="url(#arrow-amber)" />
<rect x="452" y="128" width="140" height="56" rx="8" fill="#fff8e8" stroke="#e9c783" />
<text x="522" y="152" text-anchor="middle" font-size="14" font-weight="700">Malicious</text>
<text x="522" y="171" text-anchor="middle" font-size="12">block attempt</text>
<line x1="452" y1="156" x2="366" y2="156" stroke="#16703f" stroke-width="2" marker-end="url(#arrow-green)" />
<rect x="226" y="128" width="140" height="56" rx="8" fill="#eefaf2" stroke="#a9dabd" />
<text x="296" y="152" text-anchor="middle" font-size="14" font-weight="700">Owner active</text>
<text x="296" y="171" text-anchor="middle" font-size="12">fix + resubmit</text>
<line x1="226" y1="156" x2="144" y2="156" stroke="#16703f" stroke-width="2" marker-end="url(#arrow-green)" />
<rect x="24" y="128" width="120" height="56" rx="8" fill="#eefaf2" stroke="#a9dabd" />
<text x="84" y="152" text-anchor="middle" font-size="14" font-weight="700">Escalate only</text>
<text x="84" y="171" text-anchor="middle" font-size="12">2 artifacts / 3 tries</text>
</svg>
<div class="flow">
<div class="step safe" data-step="1">
<b>Publish creates a candidate, not live-by-default latest.</b>
<span>
For updates, keep the previous clean version live until the new candidate passes
policy. For a first version, nothing is public until the candidate is clean.
</span>
<span class="pill green">clean version stays live</span>
</div>
<div class="step safe" data-step="2">
<b>Malicious result blocks the attempt.</b>
<span>
Store verdict, evidence, hash, scanner labels, and blocked status on the submitted
version. Do not promote it, index it, install it, or expose it as latest.
</span>
<span class="pill amber">candidate-only safety action</span>
</div>
<div class="step safe" data-step="3">
<b>Publisher gets email remediation, not appeal CTA.</b>
<span>
Email and dashboard explain the affected skill/version and tell them to run
<code>clawhub scan</code> locally, fix the skill/plugin, and try uploading again.
</span>
<span class="pill blue">self-service recovery</span>
</div>
<div class="step safe" data-step="4">
<b>Retries are allowed, but counted silently.</b>
<span>
Let the publisher upload fixed versions of the same skill. Do not show the attempt
count in UI, but track it server-side.
</span>
<span class="pill amber">no public counter</span>
</div>
<div class="step safe" data-step="5">
<b>Ban only when the pattern crosses threshold.</b>
<span>
Ban and send the account-level appeal email when the user has 2 distinct malicious
skills/plugins, or the same artifact is still malicious after 3 attempts.
</span>
<span class="pill red">appeals stay account-level</span>
</div>
</div>
</article>
</section>
<section class="split">
<article>
<h2>Policy Split</h2>
<table>
<thead>
<tr>
<th>Signal</th>
<th>Current consequence</th>
<th>Suggested consequence</th>
</tr>
</thead>
<tbody>
<tr>
<td>ClawScan says latest skill version is malicious</td>
<td>Hide skill, then autoban publisher.</td>
<td>Block that candidate version; keep prior clean version live when possible.</td>
</tr>
<tr>
<td>Static or VirusTotal alone flags something</td>
<td>Generally scanner evidence; should not be sole autoban trigger.</td>
<td>Keep as evidence, not automatic account action.</td>
</tr>
<tr>
<td>2 distinct malicious skills/plugins, same artifact still malicious after 3 attempts, evasion, spam, or rate-limit abuse</td>
<td>Manual ban or autoban path can disable account.</td>
<td>Account ban remains valid; email includes account appeal link.</td>
</tr>
<tr>
<td>False-positive recovery</td>
<td>External appeal can unban, then restore timestamp-matched content.</td>
<td>Before a ban, owner fixes locally with <code>clawhub scan</code> and retries upload.</td>
</tr>
<tr>
<td>Malicious first version</td>
<td>Can become a full account-ban incident.</td>
<td>Nothing is published; blocked candidate is retained as evidence/remediation context.</td>
</tr>
</tbody>
</table>
</article>
<article>
<h2>Implementation Shape</h2>
<table>
<thead>
<tr>
<th>Change</th>
<th>Likely owner</th>
</tr>
</thead>
<tbody>
<tr>
<td>Stop scheduling autoban directly from ClawScan malicious skill results.</td>
<td><code>convex/skills.ts</code></td>
</tr>
<tr>
<td>Add candidate-version blocking/promotion semantics, especially for updates.</td>
<td><code>insertVersion</code> and public skill resolution</td>
</tr>
<tr>
<td>Email publisher when a submitted version is blocked as malicious.</td>
<td>new artifact-blocked email action/template</td>
</tr>
<tr>
<td>Expose publisher-visible remediation dashboard for blocked attempts.</td>
<td><code>src/routes/dashboard.tsx</code> plus Convex query</td>
</tr>
<tr>
<td>Track silent escalation counters: distinct malicious artifacts and attempts per artifact.</td>
<td>new Convex table/fields keyed by owner + artifact/version hash</td>
</tr>
<tr>
<td>Escalate to ban only through threshold or explicit account-risk policy.</td>
<td><code>users.autoban...</code> becomes threshold/reviewer path</td>
</tr>
<tr>
<td>Align skills with package/plugin release moderation vocabulary.</td>
<td>reuse concepts from <code>packages.ts</code> / <code>packageSecurity.ts</code></td>
</tr>
</tbody>
</table>
</article>
</section>
<section class="callout" style="margin-top: 22px;">
<strong>Skill/plugin parity</strong>
<p>
Plugin/package releases already have a closer precedent for the desired model: resolve the
release scan status, block malicious/quarantined releases from download, and keep the action
scoped to the release. The skill flow should match that shape. If plugin publish attempts
currently promote or expose a malicious candidate differently than this proposal, align them
so skills and plugins both use candidate-version blocking, remediation email, silent
thresholds, and account-level bans only after escalation.
</p>
</section>
<section>
<h2 style="margin-top: 22px;">Repo Anchors</h2>
<div class="repo">
<div class="repo-item">
<b>Autoban trigger</b>
<p>
<code>convex/skills.ts:525-543</code> schedules
<code>users.autobanMalwareAuthorInternal</code> for ClawScan malicious skill verdicts.
</p>
</div>
<div class="repo-item">
<b>Account blast radius</b>
<p>
<code>convex/users.ts:2454-2542</code> hides owned skills, revokes tokens, sets
<code>deletedAt</code>, applies package bans, audits, and emails.
</p>
</div>
<div class="repo-item">
<b>Immediate latest promotion</b>
<p>
<code>convex/skills.ts:10582-10664</code> moves <code>latestVersionId</code> and tags
on semver-greater publishes.
</p>
</div>
<div class="repo-item">
<b>Async scan after publish</b>
<p>
<code>convex/lib/skillPublish.ts:313-366</code> inserts the version, then queues VT and
ClawScan.
</p>
</div>
<div class="repo-item">
<b>Existing package precedent</b>
<p>
<code>convex/lib/packageSecurity.ts:86-108</code> blocks only malicious/quarantined
package releases.
</p>
</div>
<div class="repo-item">
<b>Appeal service</b>
<p>
<code>convex/httpApiV1/usersV1.ts:658-718</code> supports service-token ban context
lookup and accepted-appeal unban.
</p>
</div>
</div>
</section>
<section class="callout" style="margin-top: 22px;">
<strong>Issue seed</strong>
<p>
Replace first-offense scanner-triggered skill autobans with candidate-version blocking:
do not publish a malicious first version, retain the previous clean latest version for
updates, email the publisher with <code>clawhub scan</code> remediation guidance, allow
fixed re-upload attempts, and reserve account bans for 2 distinct malicious artifacts, the
same artifact still malicious after 3 attempts, evasion, spam/rate-limit abuse, or
human-confirmed malicious behavior.
</p>
</section>
<footer>
Generated from local repo inspection on 2026-06-05. This is a planning artifact, not a
source change.
</footer>
</main>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment