Skip to content

Release v0.2.0 — security hardening and OpenCode V2 migration - #1

Merged
gitdamnit merged 4 commits into
masterfrom
feat/stranger-danger-v2-hardening
Oct 17, 2026
Merged

gitdamnit merged 4 commits into
masterfrom
feat/stranger-danger-v2-hardening

Conversation

@gitdamnit

@gitdamnit gitdamnit commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Release v0.2.0 — security hardening and OpenCode V2 migration

Prepares and ships strangerDanger 0.2.0. This supersedes the earlier draft:
the release-only documentation commit is now included and the PR is ready to
merge into master.

What this release is

Security hardening plus migration from the legacy @opencode-ai/plugin API to
OpenCode V2 (@opencode/plugin). 0.1.0 could only audit what was sent; 0.2.0
redacts it before transmission at the supported V2 request boundaries.

Security fixes

  1. Effective outbound protection. Registers all four V2 model-request hooks —
    session.prompt, session.context, session.generate, session.title — plus
    session.compaction and tool.execute.after. Redaction is ordering-independent:
    the fully assembled request is re-scanned immediately before dispatch.
  2. File reads never modify files. The pre-read hook is strictly read-only; a
    detected secret no longer rewrites the file on disk.
  3. Structured redaction is cycle-safe and fails closed. A self-referential tool
    output previously overflowed the stack; the hook swallowed the error and the
    entire result — including plain string secrets — passed through unredacted.
    Cycles and nesting beyond 32 levels now fail closed with an explicit marker.
  4. Tool.Error.metadata is redacted — it is surfaced to the model alongside the
    error message.
  5. Chunk-safe output scanning. Tool output is no longer truncated at 1 MB;
    large content is scanned in overlapping windows and un-inspectable content fails
    closed.
  6. The V2 event-stream audit actually fires (V2 event types are namespaced
    session.*, not message*).
  7. sd_scan input is bounded (8 MiB) so a model-invoked scan cannot exhaust
    memory or hang the server.
  8. Critical regex fix. RE2/Pcre constructs ((?i:…), (?-i:…), (?s:…),
    POSIX classes, \z) that crashed new RegExp at import time are translated.
  9. No raw secrets in CLI output; safe sd redact (atomic write, no plaintext
    backup unless opted in, backup written 0600 outside the tree).
  10. Directory installs load. OpenCode resolves a plugin directory to
    <dir>/index.{ts,js} and ignores main/exports; the package now ships a root
    index.js, so { "package": "/path/to/stranger-danger" } no longer fails with
    Plugin entrypoint not found.

Release-only changes in this PR

  • Add LICENSE (MIT) — the README and package.json already declared MIT but the
    package shipped no licence file.
  • Add SECURITY.md — disclosure process, protected surfaces, and the documented
    blind spots.
  • Add PRODUCT_REQUIREMENTS.md, DECISION_LOG.md, PROJECT_STATUS.md — canonical
    project memory; PROJECT_STATUS.md records the frozen release candidate, its
    verified commit, and the evidence.
  • package.json — add repository/bugs/homepage/author/keywords,
    publishConfig.access = public, and ship the logo the README actually references
    (strangerdanger2.jpeg, previously the package shipped the unreferenced
    strangerdanger.jpeg).
  • CHANGELOG.md — mark 0.2.0 dated; note npm publication is pending.
  • README.md — link the security policy.

No src/, tests/, dist/, scripts/, or rule changes: the release commit is
byte-identical to the verified source (see below).

Verification

The release commit adds documentation and package metadata only. Code,
tests, and build artifacts are byte-identical to the Docker-verified development
commit 9cfd8135429bffb2e5e32e97f23bec3f5b6277bd:

git diff --stat 9cfd813542..HEAD -- src tests dist scripts rules .github \
  tsconfig.json tsconfig.test.json eslint.config.js index.js .gitignore
# (empty)

Local (this PR revision)

Check Result
npm run typecheck Pass
npm run lint Pass
npm run build Pass (no dist/ drift)
npm run test:unit Pass
npm run test:integration Pass
npm run test:security Pass
npm test 322 pass / 0 fail
npm run verify:package Pass (pack → isolated install → exercise artifact)

Docker Plugin Test Lab (external)

OpenCode 2.0.21, Node 22.23.3, image opencode-plugin-lab:local
(sha256:76ebd805cd548a638b9b1508e5f16e680cd5b082cd3f3561d28f2f2301336ff1):
145 passed / 0 failed / 0 skipped for strangerDanger 0.2.0 @ 9cfd813.
Covers runtime loading, tool registration, synthetic secret detection, context and
tool-output redaction, large-output windowed scanning, fail-closed oversize
handling, allowlist behavior, secret-free audit logging, per-session isolation,
the offline model-request path, all six plugin load orders, and failure isolation.

Security

  • Verified from source and tests, and independently audited: no material
    vulnerability or fail-open path was found in the current implementation.
  • No raw secret is emitted by the CLI, the tools, or the audit log; the log stores
    metadata plus an 8-hex fingerprint only.
  • Pre-read inspection never writes to disk.

Known limitations (documented, by design)

  • Provider-native WebSocket traffic is not inspected (experimental ws.* hooks
    are not registered); plain HTTP and the HTTP fallback are covered.
  • Raw http.request bodies and provider-added content outside system/messages
    are not scanned; event.subscribe is observation-only.
  • Non-plain objects (Map/Set/Date/typed arrays) inside structured values are
    passed through; over-depth and cycles fail closed.
  • Detection is pattern/entropy based; obfuscated secrets matching no rule are not
    caught.
  • strangerDanger does not guarantee prevention of every secret leak. It protects
    the surfaces above and fails closed elsewhere.

Compatibility

OpenCode V2 runtime with @opencode/plugin >= 2.0.0 (validated against
@opencode/plugin@2.0.29); Node >= 20.

Distribution

  • GitHub release v0.2.0 (this PR).
  • npm publication is not part of this change set: the @gitdamnit scope is not
    yet published and no NPM_TOKEN is configured. publish.yml runs on v*.*.*
    tags and will attempt a publish once credentials exist.

Upgrade notes

  • 0.1.0 → 0.2.0 is a breaking API migration: configure via plugins (not plugin)
    and run an OpenCode V2 host. Rule count is 236 (221 gitleaks + 12 clean-room + 3
    entropy). No user files are rewritten on read.

Security fixes:
- File reads never modify files on disk; pre-read hook is read-only.
- Effective outbound redaction via V2 session.prompt/context/compaction hooks
  and tool.execute.after; event.subscribe is audit-only (documented).
- Chunk-safe, bounded output scanning (no 1 MB silent truncation); fails closed.
- CLI and sd_scan output contain safe metadata only (no raw secrets).
- sd redact is atomic with no plaintext backup by default; opt-in 0600 backup.
- Fix RE2/Pcre regex constructs that crashed the engine at import time.
- Keyword prefilter: ~500x faster scanning on large inputs.

V2 migration:
- @opencode/plugin + Plugin.define({ id, setup(ctx) }).
- Five tools via ctx.tool.transform (JSON Schema); hooks via ctx.*.hook.
- Config example updated from plugin to plugins.

Audit/multi-session safety:
- Project-scoped paths, session IDs, 0600/0700 perms, bounded rotation.
- Runtime state gitignored; existing audit file untracked but preserved.

Tests/CI/packaging:
- Fix test discovery; full suite (291 tests) passes.
- Add security regression tests for the above.
- Node 22 CI running typecheck, lint, build, unit/integration/security tests,
  and package validation; add eslint config and verify-package script.
- Harden all hook callbacks to never throw on malformed/unexpected events so a
  broken companion or bad event cannot cascade; keep the read-only execute.before
  contract explicit.
- Document independence guarantees, hook ownership, config/storage namespaces,
  companion-absent behavior, and the ordering-independent context-hook backstop.
- Document the single optional, read-only, semver-versioned interop surface
  (@gitdamnit/stranger-danger/engine). No companion dependency is introduced.
- Remove a stale reference to the retired CheapCode project from
  scripts/write_fixture.ts; it now writes only inside this repository.
- Add tests/plugin/independence.test.ts: no cross-plugin coupling, installs and
  runs alone, project/session isolation, no-order dependency, failure isolation,
  reload/cleanup, companion-absent behavior.

Full suite: 306 tests passing; typecheck, lint, build, and package validation pass.
Security fixes:
- Register session.generate and session.title hooks. session.context only runs
  for the agent loop; transient generation and title requests were transmitted
  uninspected.
- Redact Tool.Error.metadata, which is surfaced to the model next to the error
  message.
- Make structured redaction cycle-safe and fail closed past depth 32. A cyclic
  tool output previously overflowed the stack; the hook's catch then left the
  whole result unredacted (silent fail-open).
- Fix the V2 event-stream audit: it filtered on type.startsWith("message"), but
  every V2 event is namespaced session.*, so it never audited anything. It now
  consumes the real event shapes and skips chatty *.delta frames.
- Bound sd_scan input (8 MiB) so a model-invoked scan cannot exhaust memory.

Packaging:
- Ship a root index.js. OpenCode resolves a plugin directory to <dir>/index.{ts,js}
  and ignores main/exports, so directory configs previously failed with "Plugin
  entrypoint not found" and ran unprotected. Asserted by verify:package.

Tests/docs:
- Add tests/security-coverage.test.ts (aux request hooks, tool-error metadata,
  cycle/over-depth fail-closed, real V2 event detection, injected-limit bounds
  for the oversized/timeout branches, sd_scan cap, audit isolation).
- Correct README/CHANGELOG coverage claims, per-request hook table, size limits,
  and WebSocket/HTTP-body/structured-value limitations.

Full suite: 322 tests passing (306 baseline + 16 new); typecheck, lint, build,
package validation, and an isolated OpenCode 2.0.21 runtime run all pass.
Release-only changes for v0.2.0; no source, test, or build artifacts change.

- Add MIT LICENSE (README and package already declared MIT but shipped none).
- Add SECURITY.md: disclosure process, protected surfaces, known limitations.
- Add PRODUCT_REQUIREMENTS.md, DECISION_LOG.md, PROJECT_STATUS.md
  (canonical project memory; PROJECT_STATUS records the frozen release
  candidate, its verified commit, and the evidence).
- package.json: add repository/bugs/homepage/author/keywords, publishConfig
  access=public, and ship the logo the README actually references
  (strangerdanger2.jpeg, not strangerdanger.jpeg).
- CHANGELOG.md: mark 0.2.0 dated and note npm publication is pending.
- README.md: link the security policy.
@gitdamnit
gitdamnit marked this pull request as ready for review October 17, 2026 00:26
@gitdamnit gitdamnit changed the title Security hardening + OpenCode V2 coverage fixes (0.2.0, draft) Release v0.2.0 — security hardening and OpenCode V2 migration Oct 17, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T00:29:00.869153Z 64929cb Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@gitdamnit
gitdamnit merged commit b634ce5 into master Oct 17, 2026
1 check passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64929cbaf5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/engine/scanner.ts
Comment on lines +300 to +302
for (let start = 0; start < content.length; start += stride) {
const end = Math.min(start + windowSize, content.length);
const window = content.slice(start, end);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle matches longer than the window overlap

When an unbounded match begins more than 64 KiB before a window boundary and ends after it—for example, a quoted Base64 value larger than 1 MiB—the first window lacks the closing quote and subsequent windows lack the opening assignment, so every call to scan(window, ...) misses it. The function then returns complete: true with the entire secret unchanged. Several entropy and clean-room rules are unbounded, so either carry partial matches across windows or fail closed when a possible match reaches a window edge.

Useful? React with 👍 / 👎.

Comment on lines 115 to 117
if (findings.length === 0) {
console.log("No secrets found. File unchanged.");
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check incomplete scans before reporting no secrets

If scanAndRedactBounded rejects an input as oversized, or times out/errors before collecting a finding, it returns complete: false with an empty findings array. This branch consequently prints “No secrets found” and exits successfully without reaching the warning below, misrepresenting an uninspected file as clean. Check complete first and report a non-successful/incomplete scan instead.

Useful? React with 👍 / 👎.

Comment on lines +79 to +81
const prompt = event.prompt;
if (!prompt || typeof prompt.text !== "string") return;
prompt.text = redactText(prompt.text, "message", withSession(ctx, event.sessionID));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Adjust attachment mentions after rewriting prompt text

When a prompt contains a detected secret before a file or skill mention, replacing the secret changes the text length but leaves the prompt's attachment mention offsets unchanged. OpenCode's prompt-hook documentation explicitly requires updating or removing offsets after rewriting text; otherwise attachment resolution can target the wrong span or reject admission. Preserve the old/new offset mapping or clear affected mentions when redacting.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant