Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,43 @@ on:
- master

jobs:
test:
name: Test and Build
verify:
name: Typecheck, lint, build and test
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Node
uses: actions/setup-node@v4
with:
bun-version: latest
node-version: 22
cache: npm

- name: Install dependencies
run: bun install
run: npm ci

- name: Compile detection rules from TOML
run: npm run build:rules

- name: Typecheck
run: bun run typecheck --if-present
run: npm run typecheck

- name: Lint
run: bun run lint --if-present

- name: Test
run: bun test
run: npm run lint

- name: Build
run: bun run build --if-present
run: npm run build

- name: Unit tests
run: npm run test:unit

- name: Integration tests
run: npm run test:integration

- name: Security regression tests
run: npm run test:security

- name: Package validation (pack, install, exercise the artifact)
run: npm run verify:package
32 changes: 20 additions & 12 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,39 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Setup Node for npm publishing
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
registry-url: https://registry.npmjs.org/

- name: Install dependencies
run: bun install --frozen-lockfile
run: npm ci

- name: Compile detection rules from TOML
run: npm run build:rules

- name: Typecheck
run: bun run typecheck --if-present
run: npm run typecheck

- name: Lint
run: bun run lint --if-present
run: npm run lint

- name: Test
run: bun test
- name: Unit tests
run: npm run test:unit

- name: Integration tests
run: npm run test:integration

- name: Security regression tests
run: npm run test:security

- name: Build
run: bun run build --if-present
run: npm run build

- name: Package validation
run: npm run verify:package

- name: Publish to npm
run: npm publish --provenance --access public
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ coverage/
.env.*
*.log
.DS_Store

# strangerDanger runtime state (audit logs, session data)
.opencode/state/
7 changes: 0 additions & 7 deletions .opencode/state/strangerdanger-audit.jsonl

This file was deleted.

105 changes: 105 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,110 @@
# Changelog

## 0.2.0 (2026-10-16)

Security hardening and OpenCode V2 migration. Published as a GitHub release;
npm publication of `@gitdamnit/stranger-danger` is pending registry setup
(install from the repository in the meantime).

### Security fixes

- **File reads never modify files.** The pre-read hook no longer rewrites source
files on disk when it detects a secret. Redaction now happens at the supported
V2 result/request boundaries.
- **Effective outbound protection.** Migrated to V2 `session.prompt`,
`session.context`, and `session.compaction` hooks so prompts, tool results,
compaction transcripts, and auxiliary model requests are redacted *before*
transmission. The V1 `event` handler only audited after the fact.
- **Chunk-safe output scanning.** Tool output is no longer truncated at 1 MB.
Large content is scanned in overlapping windows; un-inspectable content fails
closed with an explicit marker.
- **No raw secrets in CLI output.** `sd scan --json` and human output report safe
metadata (rule, description, source, line/column, fingerprint, redaction token).
- **Safe `sd redact`.** Atomic write (temp + fsync + rename). No plaintext
`.backup` in the working tree by default; opt in with `--backup`/`--backup-dir`,
which writes a `0600` backup outside the tree.
- **Critical regex fix.** RE2/Pcre inline-flag groups (`(?-i:…)`, `(?i:…)`,
`(?s:…)`), POSIX classes, and `\z` — which crashed `new RegExp` at import time
and made the whole engine unusable — are translated to valid JavaScript.
- **Performance.** Whole-content keyword prefilter; ~500× faster on large inputs.
- **Auxiliary requests are now covered.** `session.context` only runs for the
agent loop. The plugin now also registers `session.generate` (transient
`ctx.session.generate` requests) and `session.title` (title generation), which
had been sent uninspected.
- **Tool error metadata is redacted.** `Tool.Error.metadata` is surfaced to the
model alongside the message; it is now scanned like the rest of the result.
- **Structured redaction is cycle-safe and fails closed.** A self-referential
tool output previously overflowed the stack; the thrown error was swallowed by
the hook, leaving the *entire* result — including plain string secrets —
unredacted. Cyclic and over-deep (>32 levels) branches now fail closed with an
explicit marker instead.
- **V2 event-stream audit actually fires.** The handler filtered on
`type.startsWith("message")`, but every V2 event is namespaced `session.*`
(`session.text.ended`, `session.tool.success`, …), so it never audited anything.
It now consumes the real event shapes (skipping chatty `*.delta` frames).
- **`sd_scan` input is bounded** (8 MiB) so a model-invoked scan cannot exhaust
memory or hang the server.

### OpenCode V2 migration

- Replaced `@opencode-ai/plugin` with `@opencode/plugin` (peer dependency).
- Plugin is now `Plugin.define({ id: "gitdamnit.stranger-danger", setup(ctx) })`.
- Five tools migrated to JSON Schema and `ctx.tool.transform`: `sd_status`,
`sd_audit`, `sd_allowlist_add`, `sd_allowlist_show`, `sd_scan`.
- Hooks migrated: `tool.execute.before/after`, `session.prompt`,
`session.context`, `session.compaction`, `event.subscribe`.
- Config example updated from `plugin` to `plugins`.

### Audit logging and multi-session safety

- Project-scoped audit path under `.opencode/state/`; hooks require an explicit
per-project path (no shared mutable process-global state).
- Session IDs recorded where available; audit files are `0600`, directories `0700`.
- Size-bounded rotation to `.1`/`.2`/`.3` (never truncates records away).
- Runtime state is Git-ignored; the previously tracked audit file is untracked
but preserved on disk.

### Independence & interoperability standard

- Confirmed and documented plugin independence: no cross-plugin imports, no
shared database/configuration, no required initialization order, no companion
dependency. Tools are `sd_`-namespaced; storage and config are namespaced.
- Hardened hook callbacks to never throw on malformed/unexpected events, so a
broken companion or bad event cannot cascade.
- Documented the ordering-independent guarantee (the `session.context` hook is
the backstop) and the single optional, read-only, semver-versioned interop
surface (`@gitdamnit/stranger-danger/engine`).
- Removed a stale reference to the retired CheapCode project from
`scripts/write_fixture.ts`; it now writes only inside this repository.
- Added an independence test suite (isolated instances, session isolation,
no-order-dependency, failure isolation, reload/cleanup, companion-absent
behavior, absence of cross-plugin coupling).

### Reliability, tests, packaging, CI

- Fixed test discovery (referenced a non-existent `tests/integration.ts`;
root-level suites were never run). Full suite now runs and passes.
- Added regression coverage for read-only file access, outbound redaction,
boundary-crossing secrets, large outputs, secret-free JSON, safe backups,
allowlist behavior, audit integrity, and V2 hook/tool wiring.
- `zod` import removed (was undeclared); `@opencode/plugin` declared.
- CI rewritten to run typecheck, lint, build, unit, integration, security, and
package-validation steps on Node 22. Added `eslint.config.js` and
`scripts/verify-package.mjs`.
- **Root `index.js` entrypoint.** OpenCode resolves a plugin *directory* to
`<directory>/index.{ts,js}` and ignores `main`/`exports`, so a
`{ "package": "/path/to/stranger-danger" }` entry previously failed to load with
`Plugin entrypoint not found` — the session ran unprotected. The package now
ships a root `index.js` (also published via `files`).
- Added `tests/security-coverage.test.ts` covering the fixes above: auxiliary
request hooks, tool-error metadata, cycle/over-depth fail-closed behavior,
real V2 event detection, injected-limit bounds for the oversized/timeout
branches, the `sd_scan` cap, and two-instance audit isolation.
- Verified on the real runtime (OpenCode `2.0.21`, isolated server + disposable
project + loopback mock provider): control run transmits the synthetic secret;
plugin run does not, emits a redaction token, and writes a secret-free audit
record.

## 0.1.0 (2026-05-08)

Initial release.
Expand Down
144 changes: 144 additions & 0 deletions DECISION_LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Decision Log

## How to Use This Log

1. Copy the block below to the end of the `## Decisions` section.
2. Use the next free four-digit ID. IDs are never reused and never renumbered.
3. Date the record the day the decision was actually made (`YYYY-MM-DD`).
4. Record the alternatives you rejected and why.
5. Never rewrite history. If a decision changes, append a superseding record.

```markdown
### D-0001 — Short imperative title

- **Date:** YYYY-MM-DD
- **Status:** Accepted
- **Context:** the situation and the constraint that forced a choice
- **Alternatives Considered:** option A (rejected because ...), option B (rejected because ...)
- **Decision:** what was decided, in one or two sentences
- **Rationale:** why this option wins, including what it costs
- **Consequences:** what follows from it — new constraints, follow-up work, risks
- **Supersedes:** None
```

Allowed statuses: `Proposed`, `Accepted`, `Superseded`, `Deprecated`, `Rejected`.

## Decisions

### D-0001 — Enforce redaction at the OpenCode V2 session.* request hooks

- **Date:** 2026-05-13
- **Status:** Accepted
- **Context:** 0.1.0 scanned files and audited after the fact from the V1 `event`
handler. That cannot prevent disclosure: the event stream is observational and
arrives after a request has been admitted.
- **Alternatives Considered:** keep the V1 `event` handler (rejected: post-hoc,
cannot stop transmission); rewrite `http.request` bodies (rejected: not a
supported stable surface and it breaks provider request shaping); patch files
on read (rejected: corrupts the user's working tree).
- **Decision:** Register all four V2 model-request hooks (`session.prompt`,
`session.context`, `session.generate`, `session.title`), plus
`session.compaction` and `tool.execute.after`, and redact the assembled
request immediately before dispatch.
- **Rationale:** These are the supported boundaries that run before
transmission, and registering all of them makes redaction independent of the
order in which other plugins run.
- **Consequences:** Enforcement depends on OpenCode V2 exposing these hooks;
surfaces outside `system`/`messages` (raw HTTP bodies, provider WebSockets) are
out of scope and documented as such.
- **Supersedes:** None

### D-0002 — The pre-read hook is strictly read-only

- **Date:** 2026-05-13
- **Status:** Accepted
- **Context:** The V1 pre-read hook rewrote source files on disk when it detected
a secret, silently modifying the user's working tree.
- **Alternatives Considered:** keep rewriting files (rejected: data loss and
corruption, unacceptable for an inspection hook); delete the file (rejected:
far worse); warn only (rejected: does not protect the model context).
- **Decision:** `tool.execute.before` detects and audits only; it never writes.
Redaction happens at the result/request boundaries instead.
- **Rationale:** An inspection hook must not have side effects. The result and
request boundaries already cover what the model sees.
- **Consequences:** The on-disk file still contains the secret; users rely on
`sd redact` or their own tooling to remediate the file. This is stated in the
README.
- **Supersedes:** None

### D-0003 — Fail closed on content that cannot be inspected

- **Date:** 2026-05-16
- **Status:** Accepted
- **Context:** A cyclic tool output overflowed the stack; the hook swallowed the
error and the entire result — including plain string secrets — passed through
unredacted. Truncating large output had the same fail-open shape.
- **Alternatives Considered:** fail open (rejected: leaks silently); truncate at
a size limit (rejected: a secret past the limit leaks); throw from the hook
(rejected: would break the runtime and companion plugins).
- **Decision:** Bounded, windowed scanning with explicit fail-closed markers.
Oversize, window timeout/error, cycles, and nesting beyond 32 levels all
replace the uninspectable content with a `[REDACTED:...]` marker.
- **Rationale:** Losing content is recoverable; leaking a credential is not.
- **Consequences:** Very large or exotic payloads lose content and gain a marker.
The marker is documented and visible to the user and the model.
- **Supersedes:** None

### D-0004 — Audit records store metadata and an 8-hex fingerprint, never the secret

- **Date:** 2026-05-16
- **Status:** Accepted
- **Context:** An audit log that contains the matched value is itself a secret
store, and it is easy to attach to a bug report by accident.
- **Alternatives Considered:** store the raw match (rejected: creates a new
credential store); store nothing (rejected: no operator visibility); store a
full SHA-256 (rejected: unnecessary precision, still correlatable).
- **Decision:** Record rule id, description, source, file path, optional session
id, and an 8-hex fingerprint of the match. Never the match or the redaction
token body.
- **Rationale:** Operators can triage and allowlist by fingerprint without a log
that is dangerous to share.
- **Consequences:** The log is metadata-only; `0600` files in a `0700` directory,
with size-bounded rotation that preserves records.
- **Supersedes:** None

### D-0005 — Remain independent of companion OpenCode plugins

- **Date:** 2026-05-16
- **Status:** Accepted
- **Context:** strangerDanger is developed alongside Git That Shit and Checkpoint
Orchestrator in a shared test lab, which invites accidental coupling.
- **Alternatives Considered:** integrate directly with the companions (rejected:
couples release cycles and creates load-order dependencies); share a database
or config namespace (rejected: cross-contamination and mixed audit records).
- **Decision:** No cross-plugin imports, no shared state, no required load order.
The only interop surface is the optional, read-only, semver-versioned
`@gitdamnit/stranger-danger/engine` export.
- **Rationale:** A security plugin that can be broken by, or break, an unrelated
plugin is not trustworthy, and ordering-independent redaction (D-0001) makes
coupling unnecessary.
- **Consequences:** The companions are never required; failure isolation is
mandatory in every hook; the independence test suite guards this.
- **Supersedes:** None

### D-0006 — Release v0.2.0 on GitHub; treat npm publication as a separate, manual step

- **Date:** 2026-10-16
- **Status:** Accepted
- **Context:** The repository contains `.github/workflows/publish.yml`, which
publishes to npm on any `v*.*.*` tag push using the `NPM_TOKEN` secret. No such
secret (or repository variable) is configured, and the package is not yet
present on the npm registry.
- **Alternatives Considered:** publish to npm as part of this release (rejected:
registry ownership/authentication is not established and publication is
explicitly out of scope for this session); delete or disable `publish.yml`
(rejected: it is the intended release path once credentials exist).
- **Decision:** Create the `v0.2.0` annotated tag and GitHub release at the
verified commit. Do not publish to npm. Record the exact npm publication
checklist for a later, explicitly authorized run.
- **Rationale:** GitHub publication is fully authorized and verifiable now; npm
publication requires credentials and registry setup that do not exist.
- **Consequences:** Pushing the tag triggers `publish.yml`, which will fail at
the npm step because `NPM_TOKEN` is unset. This is expected and does not affect
the GitHub release. npm distribution must not be reported as complete.
- **Supersedes:** None
Loading
Loading