agentsclimarketplace

Audit

Skill kevzlou7979/pinta/.claude/skills/audit

Use when the user wants a security + performance audit of the Pinta Chrome extension. Walks the manifest, content scripts, service worker, message passing, WebSocket / HTTP layer, IndexedDB store, composite + screenshot pipeline, and Svelte 5 runes usage; produces a prioritized findings list with file:line refs and concrete fixes.From its SKILL.md

Install
npx -y skills add kevzlou7979/pinta --skill audit

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

15.9 KB, as published. Nobody here has run it

/audit

Audit the Pinta Chrome extension (extension/) for security and performance regressions. Output a tight findings report — severity, what, where (file:line), why it matters, and the concrete fix. Skip nitpicks that don't change behavior or cost.

0. Sanity check

You're auditing Pinta's Chrome extension, not the companion or the landing page. The relevant tree is extension/src/:

background/    service-worker.ts, screenshot.ts
content/       overlay.ts (entry), Overlay.svelte, selector.ts,
               capture.ts, Canvas.svelte, ElementEditor.svelte,
               CommentInput.svelte, tools/draw.ts, state.svelte.ts
lib/           state.svelte.ts, ws-client.ts, companions.ts,
               composite.ts, local-store.ts, url-patterns.ts,
               theme.svelte.ts, format-clipboard.ts
popup/         Popup.svelte + index.html
sidepanel/     App.svelte, AnnotationCard.svelte, SessionHistory.svelte,
               StatusPill.svelte, index.html
styles/        app.css
manifest.config.ts
tailwind.config.js
vite.config.ts

If a finding doesn't apply to the extension, drop it.

1. Auto-load Svelte 5 conventions

Before reviewing reactivity / runes / $effect usage, fetch Svelte's official AI guidance so your review aligns with current Svelte 5 idiom:

WebFetch https://svelte.dev/docs/ai/overview
WebFetch https://svelte.dev/llms-medium.txt   (optional, large)

Pay attention to:

  • $state, $derived, $effect, $props, $bindable semantics.
  • Cleanup functions returned from $effect callbacks.
  • mount() vs new Component() for Svelte 5.
  • When {@html …} is acceptable (constant data only, never user input).

2. Security review checklist

Walk each item. For every hit, write a finding (S<n>) with severity critical / high / medium / low.

2.1 Manifest surface (manifest.config.ts)

  • permissions and host_permissions — flag anything wider than required. <all_urls> is justified by the product but should be disclosed in the store listing.
  • content_scripts.matches — confirm <all_urls> and that there's a user-controlled opt-out at runtime if needed.
  • web_accessible_resources — anything exposed should not leak data.
  • content_security_policy — MV3 default is script-src 'self'; object-src 'self'. Note any extensions that loosen it, especially unsafe-eval / unsafe-inline.

2.2 Content script + Shadow DOM (content/)

  • overlay.ts — Shadow DOM isolation is mandatory; any appendChild to document.body instead of the shadow root is a leak.
  • Event-trapping list (mousedown / pointerdown / focusin etc.): make sure host-page handlers can't see synthetic events that originated inside Pinta.
  • Any innerHTML on an element under the host page → flag.
  • selector.ts escape() fallback: confirm CSS.escape is preferred and the fallback only escapes non-[\w-] chars.
  • capture.ts outerHTML truncation (HTML_TRUNCATE) — must be enforced; oversized HTML risks performance + privacy bleed.
  • nearbyText collection — bound depth (NEARBY_LEVELS) and length per level (NEARBY_TEXT_MAX). No unbounded walk.

2.3 Service worker + screenshots (background/)

  • chrome.scripting.executeScript — every call must pass typed args, not string-template a function body. Inspect every func: callsite.
  • chrome.tabs.captureVisibleTab rate limit (2/s) → must be respected, else you flood the tab. SETTLE_MS = 600 covers it.
  • The bitmap-stitch step in stitch() keeps every viewport bitmap in memory simultaneously. Long pages (>20 viewports) can OOM the worker.
  • OffscreenCanvas → convertToBlob({ type: "image/png" }) — flag if PNG is not necessary (JPEG would shrink payload by 5–10×).

2.4 Message passing (onMessage listeners)

  • Every chrome.runtime.onMessage handler should validate msg.type with a string equality and discriminate on it before reading other fields. Anything that runs .executeScript or fetches secrets without type-checking the message → critical.
  • Origin check: only accept messages from the extension's own pages/content scripts. Verify sender.id === chrome.runtime.id (or rely on Chrome's default behavior — the API only delivers same-extension messages). If externally_connectable is set in the manifest, this becomes load-bearing.

2.5 WebSocket / HTTP to companion (lib/ws-client.ts, lib/companions.ts)

  • Endpoint 127.0.0.1 only — flag any 0.0.0.0 or LAN-IP.
  • JSON.parse(event.data) — must be wrapped in try/catch (it is).
  • Reconnect backoff — must be capped (it is, at 30s).
  • discoverCompanions probes ports 7878–7898. Flag if the range grows or the timeout (PROBE_TIMEOUT_MS) is removed — closed ports must not hang the discovery.
  • Per-probe AbortController — must be used so a slow port doesn't block the whole scan.
  • No auth on companion calls — acceptable for localhost-only, but flag if the extension ever fetches non-localhost URLs.

2.6 IndexedDB / localStorage (lib/local-store.ts, lib/theme.svelte.ts)

  • Only write to IDB / localStorage; never eval data read out.
  • Quota errors must be handled (try/catch around transaction/put).
  • Theme is non-sensitive; sessions in IDB include outerHTML + screenshots. Confirm there is no sync to remote storage.

2.7 Svelte template injection

  • {@html …} callsites — every one must be bound to a constant, not to user input. The TOOLS SVG strings are fine (constants); flag any others.
  • Reactive bind:value on inputs whose value flows into a <pre> or <code> — Svelte escapes by default, but if the value is later passed to innerHTML or dangerouslySetInnerHTML equivalents, that's bad.

2.8 Third-party resources (HTML entries)

  • Each <link href="https://fonts.googleapis.com/..."> is an external resource fetched at runtime under the extension's origin. Flag if any endpoint isn't fonts.googleapis.com / fonts.gstatic.com.
  • integrity="sha384-..." — recommend on long-lived external CSS/JS.

2.9 Data exfiltration paths

  • Build a mental model: an annotated session contains

    • selector (low risk)
    • outerHTML truncated to 2KB (medium risk — may include tokens in attrs/inline scripts)
    • nearbyText (medium risk — may include PII)
    • composited screenshot (medium risk — visible page state)
    • the user's free-form comment (low risk).

    Confirm there is no path that ships these to any non-localhost endpoint.

2.10 AI / prompt-injection threat surface (the whole pipeline)

Pinta's core function — feed annotations + scraped page content + screenshots + third-party module agent.md into a coding agent that edits code and runs commands — makes indirect prompt injection its top risk (OWASP LLM01, #1 for the third year running). This is the one section that deliberately looks past extension/ into companion/src/ and skill/pinta/SKILL.md. Write findings AI<n>.

First, refresh the threat landscape — don't rely on training data:

WebSearch  "OWASP LLM Top 10 <current-year> indirect prompt injection agent"
WebSearch  "prompt injection coding agent / browser agent <current-year> new techniques"

Fold anything new (multimodal injection, memory poisoning, tool-abuse chains, agent-hijack CVEs) into the checks below, and note in the report which checks were added this run so the skill can be updated.

  • Untrusted-field coverage. Every free-form string that reaches the agent must be listed as DATA in skill/pinta/SKILL.md §3.6's table. Diff the wire payload the agent reads (companion/src/store.ts session shape, shared/src/types.ts) against that table — any new field (a new annotation kind, a new queryComment shape, a new module op) that carries user/page text and is not in §3.6 is a finding.
  • Multimodal channel. Screenshots + reference images reach the agent as raw vision input, uninspected by the text secret-scrubber. Confirm §3.6 has an explicit "image-borne text is DATA" rule (rule 6) and that no new image path bypasses it.
  • autoApply is the only confirm-skip authority. Grep for every place autoApply is set (companion/src/ws.ts, store.ts). It must come from the extension checkbox only — never inferred from comment / query / agent.md / module-settings prose. Query-only sessions (module.query.submit) may be autoApply:true but must stay read-only per §7 — verify the skill still forbids source edits on a single-query-annotation session.
  • Third-party module sandbox (§7.12 — highest-risk path). Confirm: module-id path-traversal guard (assertSafeModuleId), agent.md size cap, default-deny capability filtering (a grant is dropped unless the manifest declared it), and that the skill's compliance reassertion + capability gate still treat agent.md as DATA that cannot flip autoApply, add network/run-tool scope, or escape projectRoot.
  • Companion trust boundary. The server is unauthenticated and gated only by localhost bind + Origin allowlist. Confirm mutating routes reject cross-origin browser writes, and that module install/uninstall requires the chrome-extension:// origin (not just no-Origin) so a local process can't install a capability-bearing module behind the consent UI. Flag any new write route that a no-Origin caller can reach which grants capability or auto-applies edits.
  • Secret-scrubbing at capture. extension/src/content/capture.ts scrubInlineSecrets must still redact tokens/keys/JWTs from outerHTML + nearbyText before they leave the page. Flag any new captured field that skips the scrubber.
  • Enforcement is prompt-level. Note explicitly that text-injection defense relies on the model honoring §3.6 (there is no out-of-band tagging of untrusted content). Recommend keeping §3.6 / §7.12 concrete and example-driven, and keeping human-in-the-loop (autoApply off) as the default for any capability-bearing or cross-boundary action.

3. Performance review checklist

For each, write a finding (P<n>) with severity high / medium / low.

3.1 Service-worker memory

  • Long-page screenshot stitch (stitch()): bitmaps are held in an array until composite finishes. Flag if there's any way to release earlier (process+release per slice, then concat blobs).
  • The 30s service-worker idle-kill in MV3 — long stitches can race it. Capture timing should be < 25s for a "tall" page.

3.2 Companion discovery

  • 21 ports × PROBE_TIMEOUT_MS (250ms) = up to 5.25s wall-clock if every port hangs at the limit. Closed ports respond instantly; open non-Pinta services don't. The current parallel scan is correct — flag if it goes serial.
  • app.rescan() runs on every tab activation + URL change. Flag if it re-discovers when the active companion's URL still matches.

3.3 Reactivity / Svelte 5

  • $derived chains that recompute on every keystroke (canSubmit, hasDrawingAnnotation, matchesSelected) should depend on the smallest possible inputs.
  • $effect callbacks should return cleanup functions where they subscribe to anything.
  • bind:value on a <textarea> inside an {#each} block over many annotations — every keystroke triggers re-derivation; for V1 it's fine but flag at 100+ annotations.

3.4 Network / payload size

  • composite.ts outputs PNG via canvas.toDataURL("image/png"). PNGs for full-page screenshots can be 1–5 MB. JPEG q=85 is usually acceptable and ~10× smaller. Flag the choice.
  • Annotations with images: AnnotationImage[] ship base64 to the companion over WS. Confirm there's a per-image size cap.

3.5 Bundle size

  • Run the build and inspect extension/dist/assets/*.js:

    npm run build --workspace @pinta/extension 2>&1 | tail -30
    

    Flag any chunk > 50 KB (gzip) for the popup or sidepanel; > 20 KB for the content script. The sidepanel is allowed to be heavier than the content script.

  • If fflate (zip lib) is bundled into the content script chunk, it should be lazy-imported only inside the bundle export path.

3.6 Main-thread blocking

  • composite.ts loadImage() decodes via HTMLImageElement on the side panel main thread. Acceptable for a single-shot composite; flag if multiple are queued without yielding.
  • Per-pixel reads of large bitmaps → must use OffscreenCanvas worker.

3.7 Token economy (BYO-Claude cost)

Pinta is bring-your-own-Claude running in the user's interactive Claude Code terminal, so every annotate / AuditFlow / Test Pilot / chat run spends the user's own Claude tokens. The agent-facing payload size IS a runtime cost — treat it like bundle size, but for tokens (~1 KB of text ≈ 250 tokens; images cost vision tokens). Write findings as P<n> with severity high / medium / low.

  • Per-annotation payload caps (content/capture.ts). The agent reads outerHTML + nearbyText + comment for every annotation. Confirm the caps are still enforced and not raised: HTML_TRUNCATE (2 KB), NEARBY_LEVELS (3), NEARBY_TEXT_MAX (200). A raised or removed cap is a token regression → high. (This overlaps §2.9's privacy angle — here it's about cost: a 10 KB outerHTML is ~2.5 K tokens per pin.)
  • Screenshots to the agent. Any composited image that reaches the agent must be JPEG + downscaled, never a full-page PNG — a 1–5 MB PNG is enormous in vision tokens. Confirm images[].dataUrl is image/jpeg post-resizer (state.svelte.ts notes "practically always image/jpeg") and that the side panel doesn't ship a raw full-res composite. Flag any path that hands the agent an un-resized image → high.
  • Audit-flow query bloat (state.svelte.ts collectCustomCategoriesForQuery / collectUserChecksForQuery). The query sent to the agent should carry only the user's own checks (label/description/id) — it must NOT re-embed the built-in category tables or fix hints the agent already has from SKILL.md. Flag any duplication of static SKILL.md content into the wire payload → medium.
  • Always-on heavy fields. A new payload field that's large and sent on every request (vs lazy / opt-in) → medium. Prefer optional / on-demand for anything sizeable.
  • Unbounded growth. Any text/HTML/list collected from the page without a length or depth cap → high (both a token and a privacy leak).

The SKILL.md prompt size (loaded into the agent's context every run) is the other half of token cost, but it lives outside extension/ — /staging §4.8 gates it. This audit covers the extension-controlled payloads only.

4. Output format

Produce one ## Findings section, then a per-finding block. Example:

### S2. medium — outerHTML may carry secrets in attributes
**Where**: extension/src/content/capture.ts:35
**What**: `el.outerHTML` is captured up to 2 KB and shipped with the
session. Pages with bearer tokens or auth URLs in inline `<script>` or
`<meta>` attrs near the selected element will leak them to the agent.
**Why**: the agent is the user's own tool, but logs / history files
broaden the blast radius. Out-of-scope for normal use, in-scope for
sensitive enterprise dashboards.
**Fix**: Strip attributes from a denylist (`integrity`, `csp-nonce`,
inline `on*=` handlers, anything matching `/token|auth|secret|key/i`)
before serialization, OR add a per-project setting to disable outerHTML
capture.

End with a one-line summary: top 3 must-fix items. Skip filler.

5. Optional: turn the findings into PR(s)

If the user wants the fixes applied, group findings into atomic PRs by file group (manifest, screenshots, message passing, etc.) and ship one PR per group with the fix + a regression note.

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.