Skip to content

feat: add OpenCode hush plugin and GitLab E2E pipeline#23

Merged
byapparov merged 7 commits into
masterfrom
feat/opencode-plugin-e2e
Mar 2, 2026
Merged

feat: add OpenCode hush plugin and GitLab E2E pipeline#23
byapparov merged 7 commits into
masterfrom
feat/opencode-plugin-e2e

Conversation

@byapparov
Copy link
Copy Markdown
Contributor

Summary

  • Add tool.execute.before OpenCode plugin that blocks reads of sensitive files (.env, *.pem, credentials.*, id_rsa, .netrc, .pgpass) before the AI model executes them
  • Add GitLab CI pipeline (.gitlab-ci.yml) with two E2E scenarios: plugin blocks .env read (Scenario A), proxy redacts PII in normal files (Scenario B)
  • Add @aictrl/hush/opencode-plugin npm export, drop-in example at examples/team-config/.opencode/plugins/hush.ts, and 50 unit tests

Test plan

  • npm run build compiles cleanly (including new plugin exports)
  • npm test — all 95 tests pass (50 new plugin tests)
  • import('@aictrl/hush/opencode-plugin') resolves to { HushPlugin }
  • scripts/e2e-plugin-block.sh — plugin blocks .env, PII never in output
  • scripts/e2e-proxy-live.sh — proxy redacts PII, vault has tokens
  • GitLab CI pipeline runs successfully

🤖 Generated with Claude Code

AICtrl Bot and others added 3 commits March 2, 2026 11:44
Adds `hush redact-hook` command that runs as a Claude Code PostToolUse
hook, redacting PII from tool outputs before Claude ever sees them.
Works standalone or alongside the proxy for defense-in-depth.

- `hush redact-hook`: stdin/stdout hook handler using existing Redactor
- `hush init --hooks`: generates/merges hook config into settings.json
- CLI subcommand routing with dynamic imports (no heavy deps for hooks)
- 14 new tests (redact-hook + init integration tests)
- README: Hooks Mode section with setup, diagram, comparison table
- Team config example updated with defense-in-depth setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use `tool_response` field (not `tool_output`) matching actual payload
- Use `decision: "block"` + `reason` output format (PostToolUse has no
  outputOverride — confirmed via spec and closed GitHub issues #4635, #18594)
- Handle Read tool's nested `file.content` response shape
- Add Grep content field test case (10 tests total)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tool.execute.before plugin for OpenCode that blocks reads of
sensitive files (.env, *.pem, credentials.*, id_rsa, etc.) before
the AI model sees them. Includes GitLab CI pipeline with two E2E
scenarios: plugin blocks .env read, and proxy redacts PII in normal
files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 2, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 80.21% 292 / 364
🔵 Statements 79.19% 316 / 399
🔵 Functions 75% 42 / 56
🔵 Branches 68.83% 148 / 215
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/index.ts 77.61% 58.02% 66.66% 82.2% 15, 17, 43-48, 84, 93-95, 144, 161-162, 172, 177, 179, 189, 212-217, 225-232, 239, 258
src/middleware/redactor.ts 100% 95% 100% 100%
src/plugins/opencode-hush.ts 100% 83.33% 100% 100%
src/plugins/sensitive-patterns.ts 100% 83.33% 100% 100%
src/vault/token-vault.ts 85.34% 83.07% 85.71% 85.32% 52, 105, 142-147, 165-166, 193-195, 210-212, 254-261
Generated in workflow #88 for commit fc55b04 by the Vitest Coverage Report Action

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 2, 2026

PR Review: OpenCode Plugin + E2E Pipeline

Summary

This PR adds defense-in-depth PII protection via an OpenCode plugin (blocks sensitive file reads pre-execution) + GitLab CI E2E tests. Overall architecture is solid. Minor issues noted below.


1. Redaction Logic

✅ Strengths:

  • Comprehensive PII patterns in Redactor (email, IPv4/v6, secrets, CC, phone)
  • extractText() handles multiple tool output formats (Bash stdout/stderr, Read file.content, Grep content)

⚠️ Issues:

Location Issue Severity
src/middleware/redactor.ts:42 SECRET pattern requires 16+ chars — misses shorter secrets like token=abc123 Medium
src/commands/redact-hook.ts:42-53 Only extracts known fields; could miss PII in custom tool responses Low
src/middleware/redactor.ts No patterns for: AWS access keys (AKIA...), base64-encoded secrets, JWTs Medium

Suggestion: Add patterns for cloud provider keys:

AWS_ACCESS_KEY: /\bAKIA[0-9A-Z]{16}\b/g,
GCP_SERVICE_KEY: /\bAIza[0-9A-Za-z\-_]{35}\b/g,

2. Streaming Integrity

✅ Strengths:

  • createStreamingRehydrator() handles tokens split across SSE chunks correctly
  • Backpressure handling with res.once('drain') prevents memory bloat
  • Separate logic for SSE vs raw text modes

⚠️ Issues:

Location Issue Severity
src/vault/token-vault.ts:190-193 Partial token detection uses lastIndexOf('[') — could false-positive on JSON arrays in content Low
src/vault/token-vault.ts:92 contentBuffers accumulates without size limit — potential memory issue for very long streams Low

Suggestion: Consider a max buffer size with truncation warning for defensive streaming.


3. Security

✅ Strengths:

  • Localhost-only binding (127.0.0.1) by default
  • Optional HUSH_AUTH_TOKEN for proxy authentication
  • Plugin blocks sensitive files: .env, *.pem, *.key, credentials.*, id_rsa, .netrc, .pgpass
  • Token vault uses structuredClone for safe deep copies

⚠️ Issues:

Location Issue Severity
src/plugins/sensitive-patterns.ts:7-14 Missing: *.p12, *.pfx (PKCS#12), *.jks (Java keystores), *.asc (PGP keys) Medium
src/middleware/redactor.ts:11-13 6-char hash is short (16M space) — theoretical collision risk if attacker controls input Very Low

Suggestion: Add to SENSITIVE_GLOBS:

/\.p12$/, /\.pfx$/, /\.jks$/, /\.keystore$/, /\.asc$/,

4. Reliability

✅ Strengths:

  • Upstream error passthrough (lines 112-116)
  • 30s timeout with AbortSignal.timeout()
  • Exit code 2 for malformed hook input (per Claude Code spec)
  • Graceful cleanup in E2E scripts with trap

⚠️ Issues:

Location Issue Severity
src/index.ts:160-163 Generic 500 error loses upstream error context — hard to debug Low

Suggestion: Include upstream status in error response when available:

res.status(502).json({ error: 'Gateway forwarding failed', upstream: error.message });

Test Coverage

  • ✅ 50 new tests for plugin + hook
  • ✅ E2E scenarios cover both paths (plugin block, proxy redact)
  • ⚠️ E2E scripts marked unchecked in test plan — should verify before merge

Verdict

Approve with minor suggestions. Core security model is sound. Consider the secret pattern and keystore file additions as follow-ups.

Reviewed SHA: 14ad65d

…ve file detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 2, 2026

Code Review: Hush Semantic Gateway PR #23

Summary

This PR adds defense-in-depth capabilities via OpenCode plugin blocking + Claude Code hooks for PII redaction. Overall architecture is sound, but I identified several areas for improvement.


1. Redaction Logic

✅ Good:

  • Deterministic token hashing (SHA-256 truncated) ensures consistent rehydration across sessions
  • Sensitive key detection handles common patterns (apikey, secret, password, etc.)
  • Nested object traversal in Redactor.redact() handles deep structures

⚠️ Edge Cases / Concerns:

Issue Location Risk
JSON embedded in strings not parsed redactor.ts:78-129 PII inside "metadata": "{\"email\":\"x@y.com\"}" won't be redacted
CLI table parsing redactor.ts IP addresses in aligned columns (e.g., 10.0.0.1 active) are caught by \b boundaries, but emails with trailing spaces may have issues
IPv4 false positives redactor.ts:38 Version strings like v1.2.3.4 could match as IP — consider adding context check

Suggestion: Add a parse_nested_json option or recursive string inspection for JSON-escaped content.


2. Streaming Integrity

✅ Good:

  • createStreamingRehydrator() correctly holds back partial tokens using prefix matching (vault.test.ts:37-49)
  • SSE mode parses data: lines and accumulates content fields before rehydration
  • Backpressure handling via res.once('drain') prevents memory bloat (index.ts:143-145)

⚠️ Concerns:

Issue Location Details
Incomplete line buffering token-vault.ts:140-146 SSE mode holds incomplete lines but doesn't expose a flush mechanism — could orphan data on stream close
Content field assumption token-vault.ts:93 Only handles content, reasoning_content, partial_json — other streaming formats may miss rehydration

Suggestion: Add explicit finalize() or flush() to streaming rehydrator for clean shutdown.


3. Security

✅ Good:

  • Vault stores only in-memory with TTL (default 1hr) — no persistence to disk
  • HUSH_AUTH_TOKEN verification with Bearer prefix support (index.ts:42-51)
  • Localhost-only binding by default (HUSH_HOST=127.0.0.1)
  • Original values never logged (only token counts and types)

⚠️ Concerns:

Issue Location Risk
Vault exposure via /health index.ts:236-242 DEBUG=true exposes vaultSize — harmless, but confirm no raw value exposure
Token collision potential redactor.ts:12 6-char hex = 16M possibilities — birthday paradox suggests collisions at ~4K tokens. Consider 8 chars

4. Reliability

✅ Good:

  • Upstream error passthrough preserves status codes (index.ts:112-116)
  • 30s timeout via AbortSignal.timeout() prevents hangs
  • Reader lock release in finally block ensures cleanup (index.ts:148-150)

⚠️ Concerns:

Issue Location Details
Generic 500 on fetch failure index.ts:160-163 Loses upstream context — consider passing through error body if available
No retry logic index.ts:100-109 Transient network failures cause immediate failure

5. Plugin Implementation (New in this PR)

✅ Good:

  • sensitive-patterns.ts covers comprehensive keystore formats (PKCS#12, PFX, JKS, PGP)
  • Command parsing handles pipes (\|) and chains (&&, ;)
  • Tests cover absolute paths and basename extraction

⚠️ Concerns:

Issue Location Risk
Shell expansion bypass sensitive-patterns.ts:34-35 cat $HOME/.env or cat ~/secrets/.env won't be caught — variable expansion not handled
Glob patterns bypass sensitive-patterns.ts cat .env* would need regex tweak to match
bat command detection sensitive-patterns.ts:30 Good coverage of modern alternatives, but batcat (Ubuntu symlink) not included

Test Coverage

  • 95 total tests (50 new for plugin/hooks) — solid coverage
  • E2E scripts validate plugin + proxy integration
  • Missing: edge case tests for JSON-embedded PII and shell variable expansion

Verdict

Approve with minor suggestions. The implementation is solid for the primary use case. Consider addressing the shell expansion bypass and adding 8-char tokens for production scale.

Reviewed SHA: b2d167f

…turn 502 with upstream context

- Add Tier 1 cloud provider key patterns (AWS, GCP, GitHub, GitLab,
  Slack, Stripe, SendGrid, npm, Anthropic, OpenAI, DigitalOcean, etc.)
  and PEM private key detection to redactor
- Run cloud key patterns before generic SECRET/CREDIT_CARD to prevent
  partial matches eating prefixed keys
- Fix streaming token-vault: partial token detection now checks for
  uppercase prefix after '[' to avoid false-positives on JSON arrays
- Cap contentBuffers at 1MB per field to prevent unbounded memory growth
- Return 502 (not 500) with upstream error message for debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Expand $HOME/.env, ${VAR}/.env, and ~/.env before checking sensitive
  patterns so `cat $HOME/.env` is blocked
- Add `batcat` (Ubuntu symlink for bat) to read command detection
- 6 new test cases for shell expansion and batcat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Harden commandReadsSensitiveFile: split on <> redirects, strip shell
  metacharacters (backticks, quotes, $(), {}) before isSensitivePath,
  detect redirect patterns like `cat <.env`
- Trim whitespace in isSensitivePath
- Remove upstream message from 502 JSON response to avoid leaking
  internal infrastructure details (kept in server-side log)
- Remove dead ~/​ and $HOME expansion code — isSensitivePath already
  uses basename-only matching
- Hold back bare `[` at buffer boundary in token-vault (not just
  [A-Z_ prefixed)
- Rename TMPDIR → WORK_DIR in E2E scripts to avoid POSIX collision
- Wrap opencode calls with timeout 120 in both E2E scripts
- Replace node_modules/ artifact with cache keyed on package-lock.json
  in GitLab CI
- Set PORT=$GATEWAY_PORT explicitly in e2e-proxy-live.sh
- Add tests for shell metacharacter bypass (subshells, backticks,
  redirects, quotes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@byapparov byapparov merged commit 54c1235 into master Mar 2, 2026
8 checks passed
@byapparov byapparov deleted the feat/opencode-plugin-e2e branch March 3, 2026 10:25
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