Repository navigation
Release v0.2.0 — security hardening and OpenCode V2 migration - #1
Conversation
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.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
| for (let start = 0; start < content.length; start += stride) { | ||
| const end = Math.min(start + windowSize, content.length); | ||
| const window = content.slice(start, end); |
There was a problem hiding this comment.
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 👍 / 👎.
| if (findings.length === 0) { | ||
| console.log("No secrets found. File unchanged."); | ||
| return; |
There was a problem hiding this comment.
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 👍 / 👎.
| const prompt = event.prompt; | ||
| if (!prompt || typeof prompt.text !== "string") return; | ||
| prompt.text = redactText(prompt.text, "message", withSession(ctx, event.sessionID)); |
There was a problem hiding this comment.
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 👍 / 👎.
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/pluginAPI toOpenCode V2 (
@opencode/plugin). 0.1.0 could only audit what was sent; 0.2.0redacts it before transmission at the supported V2 request boundaries.
Security fixes
session.prompt,session.context,session.generate,session.title— plussession.compactionandtool.execute.after. Redaction is ordering-independent:the fully assembled request is re-scanned immediately before dispatch.
detected secret no longer rewrites the file on disk.
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.
Tool.Error.metadatais redacted — it is surfaced to the model alongside theerror message.
large content is scanned in overlapping windows and un-inspectable content fails
closed.
session.*, notmessage*).sd_scaninput is bounded (8 MiB) so a model-invoked scan cannot exhaustmemory or hang the server.
(?i:…),(?-i:…),(?s:…),POSIX classes,
\z) that crashednew RegExpat import time are translated.sd redact(atomic write, no plaintextbackup unless opted in, backup written
0600outside the tree).<dir>/index.{ts,js}and ignoresmain/exports; the package now ships a rootindex.js, so{ "package": "/path/to/stranger-danger" }no longer fails withPlugin entrypoint not found.Release-only changes in this PR
LICENSE(MIT) — the README andpackage.jsonalready declared MIT but thepackage shipped no licence file.
SECURITY.md— disclosure process, protected surfaces, and the documentedblind spots.
PRODUCT_REQUIREMENTS.md,DECISION_LOG.md,PROJECT_STATUS.md— canonicalproject memory;
PROJECT_STATUS.mdrecords the frozen release candidate, itsverified commit, and the evidence.
package.json— addrepository/bugs/homepage/author/keywords,publishConfig.access = public, and ship the logo the README actually references(
strangerdanger2.jpeg, previously the package shipped the unreferencedstrangerdanger.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 isbyte-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:Local (this PR revision)
npm run typechecknpm run lintnpm run builddist/drift)npm run test:unitnpm run test:integrationnpm run test:securitynpm testnpm run verify:packageDocker Plugin Test Lab (external)
OpenCode
2.0.21, Node22.23.3, imageopencode-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
vulnerability or fail-open path was found in the current implementation.
metadata plus an 8-hex fingerprint only.
Known limitations (documented, by design)
ws.*hooksare not registered); plain HTTP and the HTTP fallback are covered.
http.requestbodies and provider-added content outsidesystem/messagesare not scanned;
event.subscribeis observation-only.Map/Set/Date/typed arrays) inside structured values arepassed through; over-depth and cycles fail closed.
caught.
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
v0.2.0(this PR).@gitdamnitscope is notyet published and no
NPM_TOKENis configured.publish.ymlruns onv*.*.*tags and will attempt a publish once credentials exist.
Upgrade notes
plugins(notplugin)and run an OpenCode V2 host. Rule count is 236 (221 gitleaks + 12 clean-room + 3
entropy). No user files are rewritten on read.