Audit failed at 2026-09-09T09:18Z. Run · Transcript
- A domain returned
FAIL. audit-application.md opened with VERDICT: FAIL — read that domain's section first. A domain's own verdict outranks the merged one.
Security audit
Supply chain
VERDICT: PASS
FAIL IF results
Disclosure
node website/scripts/generate-deps.js after pnpm install --frozen-lockfile produces no diff in website/src/data/ — PASS. Ran pnpm install --frozen-lockfile (already satisfied, "Already up to date"), then the generator: "Wrote 55 dependencies to src/data/dependencies-npm.json", "Wrote 13 direct and 478 transitive Cargo dependencies to src/data/dependencies-cargo.json", "Wrote 1 bundled runtime to src/data/dependencies-runtime.json". git status --porcelain -- website/src/data/ and git diff -- website/src/data/ were both empty. Tree left clean, no revert needed.
.github/workflows/ci.yml keeps running the generator under the frozen-lockfile precondition and fails on a diff — PASS. Job build-and-test runs pnpm install --frozen-lockfile (line 25), then step "Dependency disclosure is current" (lines 30–44) runs node website/scripts/generate-deps.js and does git diff --quiet -- website/src/data/ || exit 1.
- The disclosure does not omit a shipped workspace's graph or exclude a shipped package — PASS. Derived from
pnpm-workspace.yaml's 10 packages against productDependencyFilters/excludedWorkspacePackages in website/scripts/generate-deps.js: dor→root, dormouse-lib (lib)→root, relay→root, dormouse-standalone (standalone)→root, dormouse-sidecar (standalone/sidecar)→root, dormouse (vscode-ext)→root, canopy→excluded, dormouse-website (website)→excluded, dor-lib-common and remote-lib-common→reachable workspace edges (not named directly, per the spec's own note). assertWorkspaceCoverage (website/scripts/dependency-workspaces.js) enforces this and did not throw during the generator run (an "Unclassified workspace packages" or "Excluded workspace ... is reachable" error would have aborted it with exit code 1). npx vitest run scripts/dependency-workspaces.test.js (run from website/): 1 file passed, 6/6 tests passed.
Bundled runtime
- Root
package.json has devEngines.runtime.version as an exact MAJOR.MINOR.PATCH — PASS. {"runtime":{"name":"node","version":"24.18.0","onFail":"download"}}.
standalone/src-tauri/build.rs runs --version on the binary and fails the build on mismatch, with the one permitted cross-arch skip — PASS. verify_node_version (lines 191–227) runs node_source --version, compares to read_pinned_node_version's parse of package.json, and returns Err on mismatch; it Ok-returns with a cargo:warning only when host != target (lines 197–205), matching the allowed skip.
release.yml's standalone matrix is entirely host-native (no skip is exercised in the release build) — PASS. All three build-standalone matrix entries pair native host/target: ubuntu-22.04→x86_64-unknown-linux-gnu, macos-latest→aarch64-apple-darwin, windows-latest→x86_64-pc-windows-msvc (.github/workflows/release.yml lines 25–36).
build-standalone installs the pinned runtime via node-version-file: package.json, and package.json has no volta.node or engines.node — PASS. .github/workflows/release.yml line 48 (and lines 193, 324 for the other jobs) use node-version-file: package.json; grep -n "volta\|\"engines\"" package.json returned nothing.
Cooldown and alerts
pnpm-workspace.yaml has minimumReleaseAge: 1440 — PASS (confirmed present at end of file).
.github/renovate.json has npm and cargo in enabledManagers, with minimumReleaseAge package rules for both — PASS. "enabledManagers": ["github-actions", "npm", "cargo"]; packageRules include matchUpdateTypes: patch/minor/major entries scoped to ["npm", "cargo"] at 1/3/14 days respectively.
.github/renovate.json has a vulnerabilityAlerts block with minimumReleaseAge set explicitly — PASS. "vulnerabilityAlerts": {"enabled": true, "schedule": [], "minimumReleaseAge": "1 day", "draftPR": false, "labels": [...]}, with an explanatory comment array reiterating why omission would silently drop the cooldown.
- Secret scanning, push protection, and Dependabot alerts are enabled — PASS (all three clauses individually verified via
gh api with GH_TOKEN=$AUDIT_PAT, since the default token got 403): repos/diffplug/dormouse --jq .security_and_analysis → "secret_scanning":{"status":"enabled"}, "secret_scanning_push_protection":{"status":"enabled"}; GET /repos/diffplug/dormouse/vulnerability-alerts → HTTP/2.0 204 No Content (204, not 404, meaning Dependabot alerts are on).
Qualitative findings
INFO — Newly added/upgraded runtime dependencies since the last successful audit (2026-09-08T08:52 UTC, run 34206861498; the next scheduled run started in parallel with this one). Six Renovate PRs merged same-day: tailwind-variants v3.3.1 (#594), playwright-core v1.63.0 (#593), @tauri-apps/plugin-updater v2.11.0 (#592), hono v4.13.7 (#591), @types/react-dom v19.2.7 (#590), @tauri-apps/plugin-shell v2.3.6 (#589). Checked each: tailwind-variants, @tauri-apps/plugin-updater, hono, @tauri-apps/plugin-shell are production dependencies and each commit carries a matching website/src/data/dependencies-npm.json update (verified via git show --stat); playwright-core (vscode-ext/package.json devDependencies) and @types/react-dom (four packages, all devDependencies) correctly carry no disclosure-file change since they never reach a user's machine. All six are ordinary version bumps (no dependency additions/removals), all pnpm-managed and already reflected in the clean disclosure regeneration above. No BLOCKER or WARNING.
INFO — Lockfile resolutions outside the registry: exactly one, @diffplug/xterm-addon-webgl-sdf at canopy/package.json, resolved as a GitHub Releases tarball (https://github.com/diffplug/xterm.js/releases/download/sdf-v0.20.0-sdf304.0/...tgz) with an integrity: sha512-... pin in pnpm-lock.yaml. canopy is one of the two workspaces the spec explicitly excludes as a root ("Storybook-only rendering lab no shipped build imports"), and this is exactly the fork dependency AGENTS.md's Architecture section documents by name. Confirmed no such non-registry resolution is reachable from any of the six product roots: all 55 disclosed npm entries have ordinary semver-looking version fields (no http/github substrings), and standalone/src-tauri/Cargo.lock shows every one of its 491 non-root packages sourced from registry+https://github.com/rust-lang/crates.io-index (the only package with no source line is the local dormouse crate itself). No BLOCKER or WARNING.
INFO — Install scripts in production dependencies: pnpm-workspace.yaml's allowBuilds allowlist (@swc/core: true, esbuild: true, node-pty: true, sharp: true, and explicit denials @vscode/vsce-sign: false, keytar: false) is being enforced, not just declared. node_modules/.modules.yaml after a frozen-lockfile install shows "ignoredBuilds": [] and "pendingBuilds": [] — no package with a build/install script is sitting untriaged, which is what would happen if a newly bumped dependency introduced one Renovate hadn't been taught about. Both keytar@7.9.0 and @vscode/vsce-sign@2.0.9 are present in the lockfile (transitive, packaging-tool-only) and correctly build-denied rather than absent. No BLOCKER or WARNING.
INFO — Reachable-but-undisclosed check: no gap found. dor-lib-common and remote-lib-common (workspace edges, not named roots) are exercised by the coverage assertion and its pinned test; pnpm-workspace.yaml was not touched since the last audit, so no new workspace package needs classification.
No BLOCKER or WARNING findings. All FAIL IF checks in scope were determined (none UNVERIFIABLE).
CI and secrets
VERDICT: PASS
FAIL IF results
docs/specs/security.md
- Private vulnerability reporting enabled — PASS.
gh api repos/diffplug/dormouse/private-vulnerability-reporting → {"enabled":true}.
docs/specs/security-ci.md — GitHub Actions Policies
pull_request_target appears only in tend-*.yaml — PASS. Repo-wide grep found exactly one hit: .github/workflows/tend-review.yaml:11. No other .github/workflows/** file uses it.
- Non-agent-managed workflow effective write permissions restricted to the named exceptions — PASS.
release.yml: top-level contents: read; build-standalone/build-vscode jobs add only id-token: write + attestations: write (the named exception); security-audit job adds only actions: write (the named exception); publish-vscode has no job-level block, inheriting contents: read. ci.yml and chromatic.yml each declare only contents: read, no job overrides.
Automated Maintainer (tend)
scripts/workflow-audit.test.mjs pins the tend-regen materialization boundary (mode-100644/100755-only, symlink rejection, no unrelated generated files) — PASS. node scripts/workflow-audit.test.mjs: 6/6 tests pass, including "never materializes an unrelated symlink" and "rejects symlink workflow inputs before running the generator."
workflow-audit.yaml's lower bound is the previous successful run's server-set created_at, never pusher-controlled — PASS. Read .github/workflows/workflow-audit.yaml: SINCE=$(gh api .../workflows/workflow-audit.yaml/runs?status=success...--jq '.workflow_runs[0].created_at // ""'), falling back only to a fixed 25 hours ago if no prior run exists. No --since derived from commit data.
- Admin-gating rulesets — PASS, both clauses independently verified via
GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/rulesets/16757376 and /16757382:
Merge access (16757376): target: branch, conditions.ref_name.include: ["~DEFAULT_BRANCH"], rules: [{type: update}], bypass_actors: [{actor_id: 5, actor_type: RepositoryRole}] — exact match.
Tag operations (16757382): target: tag, conditions.ref_name.include: ["~ALL"], rules: [{type: creation}, {type: update}], bypass_actors: [{actor_id: 5, actor_type: RepositoryRole}] — exact match.
dormouse-bot holds neither maintain nor admin — PASS. GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/collaborators/dormouse-bot/permission → permission: "write", role_name: "write", permissions: {admin: false, maintain: false, push: true, ...}.
- Every environment's deployment-branch-policy admits only refs admin-gated by the two rulesets — PASS, checked all four environments individually:
release-attest: v* tag only (Tag operations).
security-audit: main branch + v* tag (Merge access + Tag operations).
tend: main only (Merge access).
vscode-extension-publish: v* tag only (Tag operations).
All match the spec's "Today:" list exactly.
- Secret inventory placement — PASS, verified with one
gh api call per surface:
- Repo-level (
actions/secrets): only CHROMATIC_PROJECT_TOKEN.
- Org-level (
actions/organization-secrets): total_count: 0.
security-audit env secrets: AUDIT_PAT, CLAUDE_CODE_OAUTH_TOKEN — exact match.
tend env secrets: TEND_BOT_TOKEN, CLAUDE_CODE_OAUTH_TOKEN — exact match.
vscode-extension-publish env secrets: OVSX_PAT, VSCE_PAT — exact match.
release-attest env: secrets: [], variables: [] — empty as required.
- No
ANTHROPIC_API_KEY visible at repo or org level.
CHROMATIC_PROJECT_TOKEN present in .config/tend.yaml secrets.allowed — PASS. File contents: secrets:\n allowed:\n - CHROMATIC_PROJECT_TOKEN.
workflow-audit.yaml not missing/disabled, successful run within 48h — PASS. gh api .../actions/workflows shows it active; last 5 runs all success, most recent 2026-09-08T12:06:14Z (~21h before this audit at 2026-09-09T09:00Z).
- Every
tend-*.yaml pins max-sixty/tend at ≥0.1.19 — PASS. All 8 files pin max-sixty/tend/claude@0.2.0 and carry header # Generated by tend 0.2.0.
- Unpinned action refs confined to
tend-*.yaml — PASS. Repo-wide grep for unpinned (@vN/tag-style) uses: lines returned matches only inside tend-ci-fix.yaml, tend-mention.yaml, tend-nightly.yaml, tend-notifications.yaml, tend-review-runs.yaml, tend-review.yaml, tend-triage.yaml, tend-weekly.yaml (actions/checkout@v7, astral-sh/setup-uv@v10.0.1). Every other workflow's uses: lines carry a commit-SHA pin with a version comment (spot-checked release.yml, ci.yml, chromatic.yml, security-audit.yaml, workflow-audit.yaml).
- Agent-managed workflows' effective
GITHUB_TOKEN permissions stay within the allowed set — PASS. Read every job-level (and workflow-level, where no job override exists) permissions: block in security-audit.yaml, workflow-audit.yaml, and all 8 tend-*.yaml files: observed scopes are exactly {contents: write|read, pull-requests: write|read, actions: read, issues: write, id-token: write} — no scope outside the allowed list on any job.
default_workflow_permissions is read and can_approve_pull_request_reviews is false — PASS. GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/actions/permissions/workflow → {"default_workflow_permissions":"read","can_approve_pull_request_reviews":false}.
VS Code Extension Releases
vscode-extension-publish environment protections — PASS. GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/environments shows nonempty required_reviewers (2 named users), prevent_self_review: true on that protection rule, and top-level can_admins_bypass: false.
release.yml's VS Code publish job is bound to vscode-extension-publish, and VSCE_PAT/OVSX_PAT appear nowhere else — PASS. publish-vscode job declares environment: {name: vscode-extension-publish}; repo-wide grep for VSCE_PAT|OVSX_PAT under .github/workflows/ found both references only inside that job (lines 347, 359 of release.yml).
release.yml does not use production desktop signing secrets in CI and generates an ephemeral Tauri updater key — PASS. build-standalone runs tauri signer generate --ci --write-keys "$RUNNER_TEMP/tauri-ci-updater.key" --force and exports it as TAURI_SIGNING_PRIVATE_KEY for that job only; repo-wide grep for EV_SIGN_PIN/APPLE_SIGN_PASS under .github/workflows/ returned no hits, and the only TAURI_SIGNING_PRIVATE_KEY hit in workflows is this ephemeral-key assignment.
Desktop Releases
scripts/sign-and-deploy.sh verifies GitHub artifact attestations, verifies SHA-256 manifests, and uses PIV-backed Windows signing — PASS. verify_downloaded_artifact() runs gh attestation verify with --cert-identity/--cert-oidc-issuer/--source-ref/--source-digest, and check_sha256_manifest() runs sha256sum -c/shasum -a 256 -c; sign_windows() calls jsign --storetype PIV --storepass env:EV_SIGN_PIN.
TAURI_SIGNING_PRIVATE_KEY is env-only and EV_SIGN_PIN is passed to jsign --storepass by environment reference, not literally — PASS. sign_updates() invokes TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY" ... tauri signer sign "$bundle" (env prefix, never on the tauri argv), and both jsign calls use the literal string env:EV_SIGN_PIN as the --storepass argument (jsign resolves it from the environment itself), never the PIN's value. APPLE_SIGN_PASS does travel on notarytool's argv — this is the spec's documented, already-accepted known gap, not a new finding.
docs/specs/security-audit.md
- Every
docs/specs/security*.md spec claimed by exactly one domain, no scope names a nonexistent file — PASS. node scripts/spec-lint.mjs → spec-lint: OK (33 specs, 65 files checked); its check 16 is exactly this rule, and it passed. Manually cross-checked the 6 non-rationale security*.md files against the three domain **Scope blocks: each appears exactly once.
- Dedicated
application-security subagent, not merged into another domain's context — PASS. .github/audit/application-security.md exists standalone, scoped only to security-local.md/security-remote.md; .github/workflows/security-audit.yaml's --agents JSON defines it as a separate agent from ci-and-secrets/supply-chain.
application-security on a stronger model than the mechanical domains, in both CI and local — PASS. security-audit.yaml: top-level claude_args sets --model sonnet, and the --agents JSON overrides application-security alone with "model":"opus". scripts/security-audit-local.sh: run_domain() sets model_args="--model sonnet" unconditionally then overrides to --model opus only when domain = application-security.
.github/audit/ has every prompt file the workflow names, and the local runner uses the same files — PASS. All five files (_preamble.md, orchestrator.md, supply-chain.md, ci-and-secrets.md, application-security.md) exist; security-audit-local.sh loops over exactly that filename set at startup and fails closed if any is missing; both CI and local prompts are Read ".github/audit/_preamble.md" and ".github/audit/<domain>.md" pointers, not inlined copies.
- Union of qualitative scopes covers every top-level path — PASS.
ci-and-secrets owns .github/, .config/, .claude/, .vscode/, scripts/, website/public/, plus any secret-touching code; supply-chain owns the dependency graph, lockfile, and all of website/ except website/public/; application-security is defined as the recursive subtraction remainder, covering everything else including .impeccable/ and root files. ls -A of the repo root shows nothing outside this union (node_modules/ is gitignored and untracked, not a scope gap).
.github/audit/ and .vscode/ both inside workflow-audit.yaml's diff window — PASS. WINDOW=(.github/workflows/ .config/tend.yaml .github/audit/ .vscode/), and WINDOW_NON_WORKFLOW=("${WINDOW[@]:1}") is derived from it (not hand-duplicated), consumed identically by the commit list, own_changes(), and both classifiers' refusal checks.
Orchestration
orchestrator.md requires a non-turn-ending wait via a persisted-deadline Bash until loop, re-issued past the 10-minute cap, under a 25-minute deadline — PASS. §2 of .github/audit/orchestrator.md specifies exactly this, with DEADLINE_FILE="$RUNNER_TEMP/audit-deadline" persisted across re-issues.
security-audit.yaml's timeout-minutes: 40 exceeds the orchestrator's 25-minute wait deadline — PASS. Confirmed in the workflow file with an explanatory comment tying the two together.
- Orchestrator cannot report PASS while a subagent left no fragment — PASS. §4 of
orchestrator.md: "If no subagent returned FAIL but any domain returned INCONCLUSIVE, or a fragment is missing, empty, or has no exact verdict line, write no status file at all," landing on MISSING → INCONCLUSIVE, not PASS. security-audit.yaml's reporting step independently re-checks fragment presence and verdict-line exactness regardless of what the orchestrator wrote (defense in depth), and both paths exit non-zero.
Outcomes and reporting
- Only literal
PASS/FAIL honored, three real outcomes — PASS. case "${STATUS:-}" in PASS|FAIL) ;; *) STATUS=MISSING ;; esac in security-audit.yaml.
audit-report.md written before audit-status.txt — PASS. orchestrator.md §4 states this explicitly and orders the instructions accordingly.
- Redact-secrets step covers every published sink and fails closed — PASS. Read the step: it processes
$TRANSCRIPT, audit-report.md, and $AUDIT_FRAGMENTS (the three per-domain files, sourced from the job-level AUDIT_FRAGMENTS env var so the redactor and the reporting-step guard loop can't drift), and on any Node error runs rm -f on that exact same file set before exiting 1. node scripts/security-audit.test.mjs → 19/19 pass, including "redaction covers every published sink" and "redactor failure removes every published sink."
- Reporting step writes one note per condition, not per combination — PASS. Five independent
if [ -n "${X:-}" ]; then echo "..." >> "$NOTES"; fi blocks (DISSENTING, MISSING_FRAGMENTS, UNREADABLE_VERDICTS, INCONCLUSIVE_DOMAINS, STATUS_FILE_VERDICT), each appending independently.
- Both fragment guards run unconditionally, not gated on status — PASS. Both
for f in $AUDIT_FRAGMENTS loops run unconditionally before the single escalation block; comments in the file explicitly flag this ("Both loops now run UNCONDITIONALLY").
- Reporting step accepts only exact
VERDICT: PASS, recognizes VERDICT: FAIL as dissent (with or without suffix), flags VERDICT: INCONCLUSIVE, and treats anything else as unreadable; STATUS only literal PASS/FAIL — PASS, confirmed by direct read of the case statements plus node scripts/security-audit.test.mjs's "reporting" test group (10/10 pass, covering exact-match, prefix, whitespace, and combination cases).
- Redaction/reporting regression suite — PASS.
node scripts/security-audit.test.mjs: 19/19 tests pass (0 failing).
Environment and AUDIT_PAT
security-audit.yaml dispatch is a full release gate (dispatch + watch + needs:) — PASS. release.yml's security-audit job runs gh workflow run "$workflow" ..., then gh run watch "$run_id" ... --exit-status; publish-vscode declares needs: [build-standalone, build-vscode, security-audit].
Verify AUDIT_PAT is provisioned step present, after checkout/install, before the audit step — PASS. Confirmed at that exact position in security-audit.yaml, checking [ -n "$AUDIT_PAT" ] and failing closed (writes FAIL status, exits 1) otherwise.
security-audit.yaml itself active and producing successful runs — PASS. gh api .../actions/workflows shows state: active; last several scheduled runs all conclusion: success (one in_progress at check time, which is this run).
Qualitative findings
No BLOCKER or WARNING findings. Scope swept: .github/ (all workflows, .github/audit/), .config/tend.yaml, .claude/ (settings.json, skills, output-styles, commands), .vscode/ (tasks.json, launch.json), scripts/ (spot-checked every script that touches process.env/secrets.), and website/public/ (the Tauri updater manifest and static assets).
.vscode/tasks.json has one task (build-dormouse-vscode, a plain pnpm build:vscode shell command) and carries no "runOn": "folderOpen" or any other checkout-triggered automation — matches docs/specs/security-ci.md's statement that no such task exists today. INFO: worth re-checking on every future edit to this file, since adding runOn here is exactly the persistence class this domain watches for and it is not caught by any lint — only by this qualitative pass.
.claude/settings.json permission allowlist is narrow and read/build-oriented (agent-browser, npx tsc, magick identify, pnpm test/filtered variants, a preview MCP tool) — nothing that reaches a secret or performs a network write.
.claude/skills/* and .claude/commands/release-notes.md are prose/process guidance for the tend bot and release workflow; nothing executes with elevated privilege from these files themselves, and none references a secret value.
website/public/standalone-latest.json (the Tauri updater manifest) contains only public update-signature material (Tauri's public-key-verifiable minisign-style signatures, not secrets) and public download URLs — consistent with docs/specs/security-ci.md -> "Desktop Releases": the manifest is generated locally by sign-and-deploy.sh's sign_updates() from the (locally-held) private key, and committing the public signature is expected.
- Scripts touching
process.env/secrets. outside .github/workflows/ (scripts/csp-defaults.mjs, scripts/dogfood-vscode.mjs, scripts/free-dev-port.mjs, scripts/pairing-walkthrough/*.mjs) read only non-secret config/dev-environment variables (ports, CSP policy toggles, dev-harness flags) — no credential material observed.
- No org-level secret is shared with this repo (confirmed via API, not just spec text), so the "Org-level secrets" re-evaluation obligation in
docs/specs/security-ci.md has nothing pending.
- All redaction/reporting/local-runner/workflow-audit regression suites (
scripts/security-audit.test.mjs, scripts/workflow-audit.test.mjs, scripts/clamp-issue-body-selftest.mjs, scripts/spec-lint.mjs) ran clean in this session, giving mechanical (not just textual) confirmation for the checks they pin.
UNVERIFIABLE: none. Every FAIL IF in scope was independently determined via gh api (using AUDIT_PAT where admin scope was required), direct file reads, or running the pinned test scripts.
Application security
VERDICT: FAIL
Domain: application-security. Scope: docs/specs/security-local.md (17 FAIL IF
bullets) and docs/specs/security-remote.md (50 bullets), plus the catch-all
remainder. Run at bb5ff06 on main.
One FAIL IF clause is violated — docs/specs/security-local.md -> "Terminal
output", the bounded-and-control-stripped rule, for CwdState.host. Every other
clause in both specs PASSES. No BLOCKER-rated qualitative finding.
FAIL IF results
Lint gates, run at HEAD: spec-lint OK (33 specs), loopback-lint OK (3
listeners, 1 allowlisted), e2e-lint OK (13 rules, 1366 checks),
e2e-lint-selftest OK (20 load-bearing checks), deploy-lint OK (3 installers,
31 rules, 81 checks), deploy-lint-selftest OK (112 load-bearing checks),
installer-verify-test OK (62 checks), ps1-cmdlet-lint OK (674 calls),
ps1-cmdlet-lint-selftest OK (4 checks). Test suites: remote-lib-common 241
pass, relay 275 pass, lib remote subtree 585 pass (31 files), vscode-ext
171 pass.
docs/specs/security-local.md -> Terminal output (3 bullets)
-
L37 isKnownUnsupportedIterm2Osc still consumes OSC 52 — PASS.
lib/src/lib/terminal-protocol.ts:708-717 returns true for 52 / 52; (and
50 / 50;); reached from parseOsc at :294, which returns [] so the
sequence never re-enters visibleData (:223-228). 52 and 50 are also in
OSC_CONSUMED_IDS (:93), so an unterminated one is buffered, not forwarded.
-
L37 every parse site runs TerminalProtocolParser before pty:data leaves it
— PASS, 4 emitters, all covered. VS Code:
vscode-ext/src/message-router.ts:231-234 routes every raw onData into
getOwnerPtyStream(id).write(data); the only pty:data post is :532, fed
from createProcessedPtyStream.onChunk (:288-291). Standalone:
standalone/sidecar/main.js:46 withholds the data event, which reaches the
wire only via lib/src/host/remote/sidecar-entry.ts:256-266 ->
ownerStream(id).parsed.write(chunk) -> :167; a throw in the tap drops the
chunk rather than emitting it raw (:41-45). lib/src/lib/platform/fake-adapter.ts:456-470
parses before dataHandlers. vscode-ext/src/processed-pty-streams.ts
refuses a second parser. The three webview-side new TerminalProtocolParser
are the one-shot replay parsers the spec sanctions.
-
L38 TITLE_LIMIT bounds every retained title — PASS.
lib/src/lib/terminal-protocol.ts:79 (256), applied :333, :387, :696, :793.
-
L38 BODY_LIMIT bounds every retained body — PASS. :80 (4096), applied
:321, :334, :388.
-
L38 sanitizeText strips controls before storage — PASS. :821-828,
[\x00-\x1f\x7f-\x9f]+ -> space, then truncateText (:834-837) counting
code points, so no split surrogate; empty reduces to null.
-
L38 COMMAND_LINE_LIMIT binds after the \xNN unescape, 4x before it —
PASS. :614-617
sanitizeText(decodeOsc633Value(truncateText(rawCommand, COMMAND_LINE_LIMIT * 4)), COMMAND_LINE_LIMIT);
COMMAND_LINE_LIMIT = 2048 at :86.
-
L38 MAX_CWD_LENGTH / boundedCwdValue bound and strip every CWD source —
PASS for path and uri. lib/src/lib/terminal-state.ts:722 (4096) and
:738-743; applied in cwdFromOsc7 (:266, :275), cwdFromOsc9_9 (:290),
cwdFromDecodedPath (:679, serving osc633 / osc1337 / process / manual).
-
L38 no retained value stops being bounded and control-stripped —
FAIL, one value: CwdState.host. See BLOCKER-adjacent detail below.
cwdFromOsc7 (lib/src/lib/terminal-state.ts:263-286) bounds and strips
rawUri at :265 and re-bounds the path after its percent-decode at :274
— but :275 computes host = extractFileUriHost(rawUri) || parsed.hostname,
and extractFileUriHost (:704-708) regex-slices [^/]* out of the URI and
then safeDecodeURIComponents it. The percent-decode re-introduces control
characters after the only strip, and nothing re-sanitizes. Reproduced
against the shipped predicates:
file://ok.example?a%1Bb%5D0;PWNED%07 yields
host: "ok.example?a�b]0;PWNED�", isRemote: true. The URL parser
rejects controls inside the host, but the ?/# tail is outside the host and
the regex swallows it to end-of-string.
Retained and rendered: cwd.host is spliced into pane-header labels at
terminal-state.ts:340 and :369 and into grouping labels at :850, and is
a component of cwdIdentity (:326). It is also persisted — the whole
CwdState is written to the notepad archive on Surface closure and read back
with a type check only (lib/src/lib/notepad/archive-model.ts:137-140, no
length bound, no control strip).
Blast radius is bounded: React renders these as text nodes, so there is no
markup execution and no escape re-injection into a PTY, and the value does not
cross a boundary — dor ls sends state.cwd?.path (lib/src/components/Wall.tsx:1085)
and the remote directory sends pane.cwd?.path
(lib/src/remote/burrow/directory.ts:33), neither host. So this is header
and archive-record spoofing/corruption, not code execution. It is nonetheless
a violation of the rule as written, and the code is internally inconsistent:
the sibling path on the same line is re-bounded after its decode. One-line
fix: wrap extractFileUriHost's return in boundedCwdValue.
-
L39 OSC 8 activation cannot reach openExternal without the dialog —
PASS. lib/src/lib/terminal-lifecycle.ts:210-217 calls
event.preventDefault() then requestExternalLinkConfirmation; the only
consumer of that store is lib/src/components/ExternalLinkModalHost.tsx:24-28.
The other two openExternal callers are app chrome
(lib/src/components/Wall.tsx:1623, lib/src/components/ExternalTextLink.tsx:19).
-
L39 the dialog renders no open action for a deceptive verdict — PASS,
all three render branches. lib/src/components/ExternalLinkModal.tsx:109-156:
isDeceptive (:61) is tested first, so an openable-and-deceptive target
takes the deceptive arm (Close + copy only, :111-126); branch 2
(:128-146) is the sole onConfirm wiring; branch 3 (:147-156) is Close
alone. Initial focus is the copy button when deceptive (:81).
-
L39 the host also rejects a deceptive confirmation — PASS.
ExternalLinkModalHost.tsx:25-27 requires
status === 'openable' && verdict !== 'deceptive'.
-
L39 second pass through normalizeExternalUri at every adapter — PASS.
vscode-ext/src/message-router.ts:636-638, standalone/src/tauri-adapter.ts:419-421,
standalone/src/browser-sidecar-adapter.ts:229-231.
docs/specs/security-local.md -> Browser panes (2 bullets)
Truncated to fit: the full body is 100009 characters. The untruncated audit-report.md is in this run's audit-transcript artifact (download).
Audit failed at 2026-09-09T09:18Z. Run · Transcript
FAIL. audit-application.md opened withVERDICT: FAIL— read that domain's section first. A domain's own verdict outranks the merged one.Security audit
Supply chain
VERDICT: PASS
FAIL IF results
Disclosure
node website/scripts/generate-deps.jsafterpnpm install --frozen-lockfileproduces no diff inwebsite/src/data/— PASS. Ranpnpm install --frozen-lockfile(already satisfied, "Already up to date"), then the generator: "Wrote 55 dependencies to src/data/dependencies-npm.json", "Wrote 13 direct and 478 transitive Cargo dependencies to src/data/dependencies-cargo.json", "Wrote 1 bundled runtime to src/data/dependencies-runtime.json".git status --porcelain -- website/src/data/andgit diff -- website/src/data/were both empty. Tree left clean, no revert needed..github/workflows/ci.ymlkeeps running the generator under the frozen-lockfile precondition and fails on a diff — PASS. Jobbuild-and-testrunspnpm install --frozen-lockfile(line 25), then step "Dependency disclosure is current" (lines 30–44) runsnode website/scripts/generate-deps.jsand doesgit diff --quiet -- website/src/data/ || exit 1.pnpm-workspace.yaml's 10 packages againstproductDependencyFilters/excludedWorkspacePackagesinwebsite/scripts/generate-deps.js:dor→root,dormouse-lib(lib)→root,relay→root,dormouse-standalone(standalone)→root,dormouse-sidecar(standalone/sidecar)→root,dormouse(vscode-ext)→root,canopy→excluded,dormouse-website(website)→excluded,dor-lib-commonandremote-lib-common→reachable workspace edges (not named directly, per the spec's own note).assertWorkspaceCoverage(website/scripts/dependency-workspaces.js) enforces this and did not throw during the generator run (an "Unclassified workspace packages" or "Excluded workspace ... is reachable" error would have aborted it with exit code 1).npx vitest run scripts/dependency-workspaces.test.js(run fromwebsite/): 1 file passed, 6/6 tests passed.Bundled runtime
package.jsonhasdevEngines.runtime.versionas an exactMAJOR.MINOR.PATCH— PASS.{"runtime":{"name":"node","version":"24.18.0","onFail":"download"}}.standalone/src-tauri/build.rsruns--versionon the binary and fails the build on mismatch, with the one permitted cross-arch skip — PASS.verify_node_version(lines 191–227) runsnode_source --version, compares toread_pinned_node_version's parse ofpackage.json, and returnsErron mismatch; itOk-returns with acargo:warningonly whenhost != target(lines 197–205), matching the allowed skip.release.yml's standalone matrix is entirely host-native (no skip is exercised in the release build) — PASS. All threebuild-standalonematrix entries pair native host/target:ubuntu-22.04→x86_64-unknown-linux-gnu,macos-latest→aarch64-apple-darwin,windows-latest→x86_64-pc-windows-msvc(.github/workflows/release.ymllines 25–36).build-standaloneinstalls the pinned runtime vianode-version-file: package.json, andpackage.jsonhas novolta.nodeorengines.node— PASS..github/workflows/release.ymlline 48 (and lines 193, 324 for the other jobs) usenode-version-file: package.json;grep -n "volta\|\"engines\"" package.jsonreturned nothing.Cooldown and alerts
pnpm-workspace.yamlhasminimumReleaseAge: 1440— PASS (confirmed present at end of file)..github/renovate.jsonhasnpmandcargoinenabledManagers, withminimumReleaseAgepackage rules for both — PASS."enabledManagers": ["github-actions", "npm", "cargo"];packageRulesincludematchUpdateTypes: patch/minor/majorentries scoped to["npm", "cargo"]at 1/3/14 days respectively..github/renovate.jsonhas avulnerabilityAlertsblock withminimumReleaseAgeset explicitly — PASS."vulnerabilityAlerts": {"enabled": true, "schedule": [], "minimumReleaseAge": "1 day", "draftPR": false, "labels": [...]}, with an explanatory comment array reiterating why omission would silently drop the cooldown.gh apiwithGH_TOKEN=$AUDIT_PAT, since the default token got 403):repos/diffplug/dormouse --jq .security_and_analysis→"secret_scanning":{"status":"enabled"},"secret_scanning_push_protection":{"status":"enabled"};GET /repos/diffplug/dormouse/vulnerability-alerts→HTTP/2.0 204 No Content(204, not 404, meaning Dependabot alerts are on).Qualitative findings
INFO — Newly added/upgraded runtime dependencies since the last successful audit (2026-09-08T08:52 UTC, run 34206861498; the next scheduled run started in parallel with this one). Six Renovate PRs merged same-day:
tailwind-variantsv3.3.1 (#594),playwright-corev1.63.0 (#593),@tauri-apps/plugin-updaterv2.11.0 (#592),honov4.13.7 (#591),@types/react-domv19.2.7 (#590),@tauri-apps/plugin-shellv2.3.6 (#589). Checked each:tailwind-variants,@tauri-apps/plugin-updater,hono,@tauri-apps/plugin-shellare production dependencies and each commit carries a matchingwebsite/src/data/dependencies-npm.jsonupdate (verified viagit show --stat);playwright-core(vscode-ext/package.jsondevDependencies) and@types/react-dom(four packages, alldevDependencies) correctly carry no disclosure-file change since they never reach a user's machine. All six are ordinary version bumps (no dependency additions/removals), all pnpm-managed and already reflected in the clean disclosure regeneration above. No BLOCKER or WARNING.INFO — Lockfile resolutions outside the registry: exactly one,
@diffplug/xterm-addon-webgl-sdfatcanopy/package.json, resolved as a GitHub Releases tarball (https://github.com/diffplug/xterm.js/releases/download/sdf-v0.20.0-sdf304.0/...tgz) with anintegrity: sha512-...pin inpnpm-lock.yaml.canopyis one of the two workspaces the spec explicitly excludes as a root ("Storybook-only rendering lab no shipped build imports"), and this is exactly the fork dependencyAGENTS.md's Architecture section documents by name. Confirmed no such non-registry resolution is reachable from any of the six product roots: all 55 disclosed npm entries have ordinary semver-lookingversionfields (nohttp/githubsubstrings), andstandalone/src-tauri/Cargo.lockshows every one of its 491 non-root packages sourced fromregistry+https://github.com/rust-lang/crates.io-index(the only package with nosourceline is the localdormousecrate itself). No BLOCKER or WARNING.INFO — Install scripts in production dependencies:
pnpm-workspace.yaml'sallowBuildsallowlist (@swc/core: true,esbuild: true,node-pty: true,sharp: true, and explicit denials@vscode/vsce-sign: false,keytar: false) is being enforced, not just declared.node_modules/.modules.yamlafter a frozen-lockfile install shows"ignoredBuilds": []and"pendingBuilds": []— no package with a build/install script is sitting untriaged, which is what would happen if a newly bumped dependency introduced one Renovate hadn't been taught about. Bothkeytar@7.9.0and@vscode/vsce-sign@2.0.9are present in the lockfile (transitive, packaging-tool-only) and correctly build-denied rather than absent. No BLOCKER or WARNING.INFO — Reachable-but-undisclosed check: no gap found.
dor-lib-commonandremote-lib-common(workspace edges, not named roots) are exercised by the coverage assertion and its pinned test;pnpm-workspace.yamlwas not touched since the last audit, so no new workspace package needs classification.No BLOCKER or WARNING findings. All
FAIL IFchecks in scope were determined (noneUNVERIFIABLE).CI and secrets
VERDICT: PASS
FAIL IF results
docs/specs/security.md
gh api repos/diffplug/dormouse/private-vulnerability-reporting→{"enabled":true}.docs/specs/security-ci.md — GitHub Actions Policies
pull_request_targetappears only intend-*.yaml— PASS. Repo-wide grep found exactly one hit:.github/workflows/tend-review.yaml:11. No other.github/workflows/**file uses it.release.yml: top-levelcontents: read;build-standalone/build-vscodejobs add onlyid-token: write+attestations: write(the named exception);security-auditjob adds onlyactions: write(the named exception);publish-vscodehas no job-level block, inheritingcontents: read.ci.ymlandchromatic.ymleach declare onlycontents: read, no job overrides.Automated Maintainer (tend)
scripts/workflow-audit.test.mjspins the tend-regen materialization boundary (mode-100644/100755-only, symlink rejection, no unrelated generated files) — PASS.node scripts/workflow-audit.test.mjs: 6/6 tests pass, including "never materializes an unrelated symlink" and "rejects symlink workflow inputs before running the generator."workflow-audit.yaml's lower bound is the previous successful run's server-setcreated_at, never pusher-controlled — PASS. Read.github/workflows/workflow-audit.yaml:SINCE=$(gh api .../workflows/workflow-audit.yaml/runs?status=success...--jq '.workflow_runs[0].created_at // ""'), falling back only to a fixed25 hours agoif no prior run exists. No--sincederived from commit data.GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/rulesets/16757376and/16757382:Merge access(16757376):target: branch,conditions.ref_name.include: ["~DEFAULT_BRANCH"],rules: [{type: update}],bypass_actors: [{actor_id: 5, actor_type: RepositoryRole}]— exact match.Tag operations(16757382):target: tag,conditions.ref_name.include: ["~ALL"],rules: [{type: creation}, {type: update}],bypass_actors: [{actor_id: 5, actor_type: RepositoryRole}]— exact match.dormouse-botholds neithermaintainnoradmin— PASS.GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/collaborators/dormouse-bot/permission→permission: "write",role_name: "write",permissions: {admin: false, maintain: false, push: true, ...}.release-attest:v*tag only (Tag operations).security-audit:mainbranch +v*tag (Merge access + Tag operations).tend:mainonly (Merge access).vscode-extension-publish:v*tag only (Tag operations).All match the spec's "Today:" list exactly.
gh apicall per surface:actions/secrets): onlyCHROMATIC_PROJECT_TOKEN.actions/organization-secrets):total_count: 0.security-auditenv secrets:AUDIT_PAT,CLAUDE_CODE_OAUTH_TOKEN— exact match.tendenv secrets:TEND_BOT_TOKEN,CLAUDE_CODE_OAUTH_TOKEN— exact match.vscode-extension-publishenv secrets:OVSX_PAT,VSCE_PAT— exact match.release-attestenv:secrets: [],variables: []— empty as required.ANTHROPIC_API_KEYvisible at repo or org level.CHROMATIC_PROJECT_TOKENpresent in.config/tend.yamlsecrets.allowed— PASS. File contents:secrets:\n allowed:\n - CHROMATIC_PROJECT_TOKEN.workflow-audit.yamlnot missing/disabled, successful run within 48h — PASS.gh api .../actions/workflowsshows itactive; last 5 runs allsuccess, most recent2026-09-08T12:06:14Z(~21h before this audit at2026-09-09T09:00Z).tend-*.yamlpinsmax-sixty/tendat ≥0.1.19 — PASS. All 8 files pinmax-sixty/tend/claude@0.2.0and carry header# Generated by tend 0.2.0.tend-*.yaml— PASS. Repo-wide grep for unpinned (@vN/tag-style)uses:lines returned matches only insidetend-ci-fix.yaml,tend-mention.yaml,tend-nightly.yaml,tend-notifications.yaml,tend-review-runs.yaml,tend-review.yaml,tend-triage.yaml,tend-weekly.yaml(actions/checkout@v7,astral-sh/setup-uv@v10.0.1). Every other workflow'suses:lines carry a commit-SHA pin with a version comment (spot-checkedrelease.yml,ci.yml,chromatic.yml,security-audit.yaml,workflow-audit.yaml).GITHUB_TOKENpermissions stay within the allowed set — PASS. Read every job-level (and workflow-level, where no job override exists)permissions:block insecurity-audit.yaml,workflow-audit.yaml, and all 8tend-*.yamlfiles: observed scopes are exactly{contents: write|read, pull-requests: write|read, actions: read, issues: write, id-token: write}— no scope outside the allowed list on any job.default_workflow_permissionsisreadandcan_approve_pull_request_reviewsisfalse— PASS.GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/actions/permissions/workflow→{"default_workflow_permissions":"read","can_approve_pull_request_reviews":false}.VS Code Extension Releases
vscode-extension-publishenvironment protections — PASS.GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/environmentsshows nonemptyrequired_reviewers(2 named users),prevent_self_review: trueon that protection rule, and top-levelcan_admins_bypass: false.release.yml's VS Code publish job is bound tovscode-extension-publish, andVSCE_PAT/OVSX_PATappear nowhere else — PASS.publish-vscodejob declaresenvironment: {name: vscode-extension-publish}; repo-wide grep forVSCE_PAT|OVSX_PATunder.github/workflows/found both references only inside that job (lines 347, 359 ofrelease.yml).release.ymldoes not use production desktop signing secrets in CI and generates an ephemeral Tauri updater key — PASS.build-standalonerunstauri signer generate --ci --write-keys "$RUNNER_TEMP/tauri-ci-updater.key" --forceand exports it asTAURI_SIGNING_PRIVATE_KEYfor that job only; repo-wide grep forEV_SIGN_PIN/APPLE_SIGN_PASSunder.github/workflows/returned no hits, and the onlyTAURI_SIGNING_PRIVATE_KEYhit in workflows is this ephemeral-key assignment.Desktop Releases
scripts/sign-and-deploy.shverifies GitHub artifact attestations, verifies SHA-256 manifests, and uses PIV-backed Windows signing — PASS.verify_downloaded_artifact()runsgh attestation verifywith--cert-identity/--cert-oidc-issuer/--source-ref/--source-digest, andcheck_sha256_manifest()runssha256sum -c/shasum -a 256 -c;sign_windows()callsjsign --storetype PIV --storepass env:EV_SIGN_PIN.TAURI_SIGNING_PRIVATE_KEYis env-only andEV_SIGN_PINis passed tojsign --storepassby environment reference, not literally — PASS.sign_updates()invokesTAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY" ... tauri signer sign "$bundle"(env prefix, never on thetauriargv), and bothjsigncalls use the literal stringenv:EV_SIGN_PINas the--storepassargument (jsign resolves it from the environment itself), never the PIN's value.APPLE_SIGN_PASSdoes travel onnotarytool's argv — this is the spec's documented, already-accepted known gap, not a new finding.docs/specs/security-audit.md
docs/specs/security*.mdspec claimed by exactly one domain, no scope names a nonexistent file — PASS.node scripts/spec-lint.mjs→spec-lint: OK (33 specs, 65 files checked); its check 16 is exactly this rule, and it passed. Manually cross-checked the 6 non-rationalesecurity*.mdfiles against the three domain**Scopeblocks: each appears exactly once.application-securitysubagent, not merged into another domain's context — PASS..github/audit/application-security.mdexists standalone, scoped only tosecurity-local.md/security-remote.md;.github/workflows/security-audit.yaml's--agentsJSON defines it as a separate agent fromci-and-secrets/supply-chain.application-securityon a stronger model than the mechanical domains, in both CI and local — PASS.security-audit.yaml: top-levelclaude_argssets--model sonnet, and the--agentsJSON overridesapplication-securityalone with"model":"opus".scripts/security-audit-local.sh:run_domain()setsmodel_args="--model sonnet"unconditionally then overrides to--model opusonly whendomain = application-security..github/audit/has every prompt file the workflow names, and the local runner uses the same files — PASS. All five files (_preamble.md,orchestrator.md,supply-chain.md,ci-and-secrets.md,application-security.md) exist;security-audit-local.shloops over exactly that filename set at startup and fails closed if any is missing; both CI and local prompts areRead ".github/audit/_preamble.md" and ".github/audit/<domain>.md"pointers, not inlined copies.ci-and-secretsowns.github/,.config/,.claude/,.vscode/,scripts/,website/public/, plus any secret-touching code;supply-chainowns the dependency graph, lockfile, and all ofwebsite/exceptwebsite/public/;application-securityis defined as the recursive subtraction remainder, covering everything else including.impeccable/and root files.ls -Aof the repo root shows nothing outside this union (node_modules/is gitignored and untracked, not a scope gap)..github/audit/and.vscode/both insideworkflow-audit.yaml's diff window — PASS.WINDOW=(.github/workflows/ .config/tend.yaml .github/audit/ .vscode/), andWINDOW_NON_WORKFLOW=("${WINDOW[@]:1}")is derived from it (not hand-duplicated), consumed identically by the commit list,own_changes(), and both classifiers' refusal checks.Orchestration
orchestrator.mdrequires a non-turn-ending wait via a persisted-deadline Bashuntilloop, re-issued past the 10-minute cap, under a 25-minute deadline — PASS. §2 of.github/audit/orchestrator.mdspecifies exactly this, withDEADLINE_FILE="$RUNNER_TEMP/audit-deadline"persisted across re-issues.security-audit.yaml'stimeout-minutes: 40exceeds the orchestrator's 25-minute wait deadline — PASS. Confirmed in the workflow file with an explanatory comment tying the two together.orchestrator.md: "If no subagent returned FAIL but any domain returned INCONCLUSIVE, or a fragment is missing, empty, or has no exact verdict line, write no status file at all," landing on MISSING → INCONCLUSIVE, not PASS.security-audit.yaml's reporting step independently re-checks fragment presence and verdict-line exactness regardless of what the orchestrator wrote (defense in depth), and both paths exit non-zero.Outcomes and reporting
PASS/FAILhonored, three real outcomes — PASS.case "${STATUS:-}" in PASS|FAIL) ;; *) STATUS=MISSING ;; esacinsecurity-audit.yaml.audit-report.mdwritten beforeaudit-status.txt— PASS.orchestrator.md§4 states this explicitly and orders the instructions accordingly.$TRANSCRIPT,audit-report.md, and$AUDIT_FRAGMENTS(the three per-domain files, sourced from the job-levelAUDIT_FRAGMENTSenv var so the redactor and the reporting-step guard loop can't drift), and on any Node error runsrm -fon that exact same file set before exiting 1.node scripts/security-audit.test.mjs→ 19/19 pass, including "redaction covers every published sink" and "redactor failure removes every published sink."if [ -n "${X:-}" ]; then echo "..." >> "$NOTES"; fiblocks (DISSENTING,MISSING_FRAGMENTS,UNREADABLE_VERDICTS,INCONCLUSIVE_DOMAINS,STATUS_FILE_VERDICT), each appending independently.for f in $AUDIT_FRAGMENTSloops run unconditionally before the single escalation block; comments in the file explicitly flag this ("Both loops now run UNCONDITIONALLY").VERDICT: PASS, recognizesVERDICT: FAILas dissent (with or without suffix), flagsVERDICT: INCONCLUSIVE, and treats anything else as unreadable;STATUSonly literalPASS/FAIL— PASS, confirmed by direct read of thecasestatements plusnode scripts/security-audit.test.mjs's "reporting" test group (10/10 pass, covering exact-match, prefix, whitespace, and combination cases).node scripts/security-audit.test.mjs: 19/19 tests pass (0 failing).Environment and AUDIT_PAT
security-audit.yamldispatch is a full release gate (dispatch + watch +needs:) — PASS.release.yml'ssecurity-auditjob runsgh workflow run "$workflow" ..., thengh run watch "$run_id" ... --exit-status;publish-vscodedeclaresneeds: [build-standalone, build-vscode, security-audit].Verify AUDIT_PAT is provisionedstep present, after checkout/install, before the audit step — PASS. Confirmed at that exact position insecurity-audit.yaml, checking[ -n "$AUDIT_PAT" ]and failing closed (writesFAILstatus, exits 1) otherwise.security-audit.yamlitself active and producing successful runs — PASS.gh api .../actions/workflowsshowsstate: active; last several scheduled runs allconclusion: success(onein_progressat check time, which is this run).Qualitative findings
No BLOCKER or WARNING findings. Scope swept:
.github/(all workflows,.github/audit/),.config/tend.yaml,.claude/(settings.json, skills, output-styles, commands),.vscode/(tasks.json,launch.json),scripts/(spot-checked every script that touchesprocess.env/secrets.), andwebsite/public/(the Tauri updater manifest and static assets)..vscode/tasks.jsonhas one task (build-dormouse-vscode, a plainpnpm build:vscodeshell command) and carries no"runOn": "folderOpen"or any other checkout-triggered automation — matchesdocs/specs/security-ci.md's statement that no such task exists today. INFO: worth re-checking on every future edit to this file, since addingrunOnhere is exactly the persistence class this domain watches for and it is not caught by any lint — only by this qualitative pass..claude/settings.jsonpermission allowlist is narrow and read/build-oriented (agent-browser,npx tsc,magick identify,pnpm test/filtered variants, a preview MCP tool) — nothing that reaches a secret or performs a network write..claude/skills/*and.claude/commands/release-notes.mdare prose/process guidance for the tend bot and release workflow; nothing executes with elevated privilege from these files themselves, and none references a secret value.website/public/standalone-latest.json(the Tauri updater manifest) contains only public update-signature material (Tauri's public-key-verifiable minisign-style signatures, not secrets) and public download URLs — consistent withdocs/specs/security-ci.md-> "Desktop Releases": the manifest is generated locally bysign-and-deploy.sh'ssign_updates()from the (locally-held) private key, and committing the public signature is expected.process.env/secrets.outside.github/workflows/(scripts/csp-defaults.mjs,scripts/dogfood-vscode.mjs,scripts/free-dev-port.mjs,scripts/pairing-walkthrough/*.mjs) read only non-secret config/dev-environment variables (ports, CSP policy toggles, dev-harness flags) — no credential material observed.docs/specs/security-ci.mdhas nothing pending.scripts/security-audit.test.mjs,scripts/workflow-audit.test.mjs,scripts/clamp-issue-body-selftest.mjs,scripts/spec-lint.mjs) ran clean in this session, giving mechanical (not just textual) confirmation for the checks they pin.UNVERIFIABLE: none. Every
FAIL IFin scope was independently determined viagh api(usingAUDIT_PATwhere admin scope was required), direct file reads, or running the pinned test scripts.Application security
VERDICT: FAIL
Domain:
application-security. Scope:docs/specs/security-local.md(17FAIL IFbullets) and
docs/specs/security-remote.md(50 bullets), plus the catch-allremainder. Run at
bb5ff06onmain.One
FAIL IFclause is violated —docs/specs/security-local.md-> "Terminaloutput", the bounded-and-control-stripped rule, for
CwdState.host. Every otherclause in both specs PASSES. No BLOCKER-rated qualitative finding.
FAIL IF results
Lint gates, run at HEAD:
spec-lintOK (33 specs),loopback-lintOK (3listeners, 1 allowlisted),
e2e-lintOK (13 rules, 1366 checks),e2e-lint-selftestOK (20 load-bearing checks),deploy-lintOK (3 installers,31 rules, 81 checks),
deploy-lint-selftestOK (112 load-bearing checks),installer-verify-testOK (62 checks),ps1-cmdlet-lintOK (674 calls),ps1-cmdlet-lint-selftestOK (4 checks). Test suites:remote-lib-common241pass,
relay275 pass,libremote subtree 585 pass (31 files),vscode-ext171 pass.
docs/specs/security-local.md-> Terminal output (3 bullets)L37
isKnownUnsupportedIterm2Oscstill consumesOSC 52— PASS.lib/src/lib/terminal-protocol.ts:708-717returns true for52/52;(and50/50;); reached fromparseOscat:294, which returns[]so thesequence never re-enters
visibleData(:223-228).52and50are also inOSC_CONSUMED_IDS(:93), so an unterminated one is buffered, not forwarded.L37 every parse site runs
TerminalProtocolParserbeforepty:dataleaves it— PASS, 4 emitters, all covered. VS Code:
vscode-ext/src/message-router.ts:231-234routes every rawonDataintogetOwnerPtyStream(id).write(data); the onlypty:datapost is:532, fedfrom
createProcessedPtyStream.onChunk(:288-291). Standalone:standalone/sidecar/main.js:46withholds thedataevent, which reaches thewire only via
lib/src/host/remote/sidecar-entry.ts:256-266->ownerStream(id).parsed.write(chunk)->:167; a throw in the tap drops thechunk rather than emitting it raw (
:41-45).lib/src/lib/platform/fake-adapter.ts:456-470parses before
dataHandlers.vscode-ext/src/processed-pty-streams.tsrefuses a second parser. The three webview-side
new TerminalProtocolParserare the one-shot replay parsers the spec sanctions.
L38
TITLE_LIMITbounds every retained title — PASS.lib/src/lib/terminal-protocol.ts:79(256), applied:333,:387,:696,:793.L38
BODY_LIMITbounds every retained body — PASS.:80(4096), applied:321,:334,:388.L38
sanitizeTextstrips controls before storage — PASS.:821-828,[\x00-\x1f\x7f-\x9f]+-> space, thentruncateText(:834-837) countingcode points, so no split surrogate; empty reduces to
null.L38
COMMAND_LINE_LIMITbinds after the\xNNunescape, 4x before it —PASS.
:614-617sanitizeText(decodeOsc633Value(truncateText(rawCommand, COMMAND_LINE_LIMIT * 4)), COMMAND_LINE_LIMIT);COMMAND_LINE_LIMIT = 2048at:86.L38
MAX_CWD_LENGTH/boundedCwdValuebound and strip every CWD source —PASS for
pathanduri.lib/src/lib/terminal-state.ts:722(4096) and:738-743; applied incwdFromOsc7(:266,:275),cwdFromOsc9_9(:290),cwdFromDecodedPath(:679, servingosc633/osc1337/process/manual).L38 no retained value stops being bounded and control-stripped —
FAIL, one value:
CwdState.host. See BLOCKER-adjacent detail below.cwdFromOsc7(lib/src/lib/terminal-state.ts:263-286) bounds and stripsrawUriat:265and re-bounds the path after its percent-decode at:274— but
:275computeshost = extractFileUriHost(rawUri) || parsed.hostname,and
extractFileUriHost(:704-708) regex-slices[^/]*out of the URI andthen
safeDecodeURIComponents it. The percent-decode re-introduces controlcharacters after the only strip, and nothing re-sanitizes. Reproduced
against the shipped predicates:
file://ok.example?a%1Bb%5D0;PWNED%07yieldshost: "ok.example?a�b]0;PWNED�",isRemote: true. The URL parserrejects controls inside the host, but the
?/#tail is outside the host andthe regex swallows it to end-of-string.
Retained and rendered:
cwd.hostis spliced into pane-header labels atterminal-state.ts:340and:369and into grouping labels at:850, and isa component of
cwdIdentity(:326). It is also persisted — the wholeCwdStateis written to the notepad archive on Surface closure and read backwith a type check only (
lib/src/lib/notepad/archive-model.ts:137-140, nolength bound, no control strip).
Blast radius is bounded: React renders these as text nodes, so there is no
markup execution and no escape re-injection into a PTY, and the value does not
cross a boundary —
dor lssendsstate.cwd?.path(lib/src/components/Wall.tsx:1085)and the remote directory sends
pane.cwd?.path(
lib/src/remote/burrow/directory.ts:33), neitherhost. So this is headerand archive-record spoofing/corruption, not code execution. It is nonetheless
a violation of the rule as written, and the code is internally inconsistent:
the sibling
pathon the same line is re-bounded after its decode. One-linefix: wrap
extractFileUriHost's return inboundedCwdValue.L39
OSC 8activation cannot reachopenExternalwithout the dialog —PASS.
lib/src/lib/terminal-lifecycle.ts:210-217callsevent.preventDefault()thenrequestExternalLinkConfirmation; the onlyconsumer of that store is
lib/src/components/ExternalLinkModalHost.tsx:24-28.The other two
openExternalcallers are app chrome(
lib/src/components/Wall.tsx:1623,lib/src/components/ExternalTextLink.tsx:19).L39 the dialog renders no open action for a deceptive verdict — PASS,
all three render branches.
lib/src/components/ExternalLinkModal.tsx:109-156:isDeceptive(:61) is tested first, so an openable-and-deceptive targettakes the deceptive arm (
Close+ copy only,:111-126); branch 2(
:128-146) is the soleonConfirmwiring; branch 3 (:147-156) isClosealone. Initial focus is the copy button when deceptive (
:81).L39 the host also rejects a deceptive confirmation — PASS.
ExternalLinkModalHost.tsx:25-27requiresstatus === 'openable' && verdict !== 'deceptive'.L39 second pass through
normalizeExternalUriat every adapter — PASS.vscode-ext/src/message-router.ts:636-638,standalone/src/tauri-adapter.ts:419-421,standalone/src/browser-sidecar-adapter.ts:229-231.docs/specs/security-local.md-> Browser panes (2 bullets)Truncated to fit: the full body is 100009 characters. The untruncated
audit-report.mdis in this run'saudit-transcriptartifact (download).