fix: merge latest dev updates into main#32
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- native.ts: add extractContext + formatArgs with matchedField/matchedWord tracing for "Context Sniper" popup — shows dangerous word in context - core.ts: extend evaluatePolicy return with matchedField/matchedWord; per-field scan after dangerous word found; pass through authorizeHeadless - daemon/index.ts: gate SSE broadcast and browser open on browser config flag - LICENSE/package.json/README.md: MIT → Apache-2.0 - .github/workflows/ai-review.yml: add paths-ignore to prevent self-modification - scripts/ai-review.mjs: upgrade to claude-sonnet-4-6, max_tokens 2048 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap diff in <diff>...</diff> markers with untrusted-content notice to mitigate prompt injection - Surface truncation note in posted PR comment when diff exceeds MAX_DIFF_CHARS - Downgrade API errors to warning comments + exit 0 so Anthropic outages don't block PRs - Pin @anthropic-ai/sdk@0.78.0 and @octokit/rest@22.0.1 to prevent supply-chain drift - Add explicit permissions block (contents: read, pull-requests: write) - Exclude dependabot[bot] from triggering review - Add fetch-depth: 0 to checkout step Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… main - ai-review.yml: replace AUTO_PR_TOKEN with GITHUB_TOKEN (permissions block already scopes it correctly — no broad PAT needed) - ai-review.yml: add --ignore-scripts to npm install to block malicious postinstall hooks from transitive dependencies - sync-dev.yml: new workflow — after every push to main, merge main back into dev so release-bot version bumps don't cause recurring README conflicts on the next dev -> main PR Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move @anthropic-ai/sdk and @octokit/rest into devDependencies and switch the ai-review workflow from bare npm install to npm ci --ignore-scripts. This locks all transitive dependencies to the committed lockfile, eliminating supply-chain drift on every CI run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- context-sniper.ts (new): shared RiskMetadata type, smartTruncate,
extractContext (returns {snippet, lineIndex}), computeRiskMetadata
- native.ts: import from context-sniper, use .snippet on extractContext calls
- core.ts: add tier to evaluatePolicy returns; compute riskMetadata once in
authorizeHeadless; pass it to initNode9SaaS, askDaemon, notifyDaemonViewer
- daemon/index.ts: store and broadcast riskMetadata in PendingEntry
- daemon/ui.html: renderPayload() uses riskMetadata for intent badge, tier,
file path, annotated snippet, matched-word highlight; falls back to raw args
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the Node9 cloud responds with shadowMode:true (org is in shadow mode), print a yellow warning to stderr instead of blocking the agent. The developer sees exactly why it was flagged without being interrupted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er, and integration tests - Add Zod v3 schema validation (config-schema.ts) with clear error messages for bad config.json — catches literal newlines, invalid regex, unknown keys, bad enums - Fix silent JSON parse fallback in tryLoadConfig: bad config now warns to stderr instead of silently using DEFAULT_CONFIG (which had cloud:true causing unexpected browser/cloud popups when config was invalid) - Fix auditLocalAllow fire-and-forget killed by process.exit: audit mode path now awaits the POST so SaaS receives the event before the process exits - Gate all auditLocalAllow calls on approvers.cloud so cloud:false (privacy mode) never sends data to SaaS - Fix double browser windows when cloud+browser both enabled: RACER 3 (browser) now skips when cloudEnforced, preventing duplicate daemon /check entries - Fix calledFromDaemon guard on terminal status messages to prevent duplicate output - Add check.integration.test.ts: 20 end-to-end tests spawning real node9 check subprocess with isolated HOME dirs and in-process mock SaaS server Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…plied Previously tryLoadConfig warned about invalid fields (e.g. mode:"bad-mode") but still returned the raw object, letting them override valid values from higher-priority config layers. A project-level node9.config.json with mode:"bad-mode" would override the global mode:"audit", bypassing audit mode and triggering the full cloud approval race unexpectedly. sanitizeConfig() now drops top-level keys that fail Zod validation so invalid project configs cannot corrupt the effective merged config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l test, improve comments - Remove unreliable 200ms sleep from audit+cloud test: auditLocalAllow is awaited before process.exit so the POST is done by the time the subprocess closes; if it ever races here it would be a production bug too - Add task* wildcard test: task_drop_all_tables must be fast-pathed to allow (documents the intentional security trade-off of user-configured wildcards) - Expand runCheck docstring explaining why cwd=tmpHome is needed alongside HOME=tmpHome (avoids inheriting the repo's own node9.config.json) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…noredTools precedence tests - Add `let resolved = false` guard in runCheckAsync to prevent double-resolve when child.kill() is called on timeout (close event fires after kill) - Fix mockServer.close() in afterEach to return a Promise (was fire-and-forget) - Document NODE9_TESTING=1 behavior in file header comment - Add runCheck/runCheckAsync raw string support for malformed payload testing - Add section 10: malformed JSON payload tests (non-JSON, empty, partial JSON) - Add ignoredTools precedence test: task* wildcard + dangerous word in input documents that ignoredTools fast-path bypasses dangerousWords (by design) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CLI intentionally exits 0 on unparseable JSON (fail-open policy): a transient serialization error must not block the AI session mid-flight. The test was asserting the opposite. Updated all three malformed-payload tests to verify graceful failure (no crash/stack trace) rather than an error exit code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
core.ts:
- fix review-git-push regex: literal space → \s+ so "git push" can't bypass
- fix getConfig(): environments block was always hardcoded {} and never merged
from global/project config files; now applyLayer() accumulates environments
correctly so strict-mode env overrides actually work
examples/node9.config.json.example:
- remove dangerousWords that caused false positives; keep only mkfs + shred
(catastrophic, unambiguous — everything else handled by smartRules)
- add enterplanmode/enterworktree/exitworktree to ignoredTools
- add execute_query, query, mcp__postgres__*, mcp__github__* to toolInspection
- fix allow-readonly-bash regex: "npm run(build|test)" → "npm run (build|test)"
(was matching "runbuild"/"runtest" instead of "run build"/"run test")
- remove smartRules already covered by built-in defaults:
review-delete-without-where, block-force-push, block-drop-database, review-sudo
- remove "push"/"git" rules entries (match tool *names*, never fire for bash)
- remove non-functional environments block (was silently ignored until above fix)
- add approvalTimeoutMs:30000, version:"1.0", expanded snapshot.tools + ignorePaths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ule bypass, tests core.ts: - move mergedEnvironments declaration before applyLayer closure so the captured variable is clearly in scope (was hoisted at runtime but misleading to read) - replace unsafe Partial<EnvironmentConfig> spread with field-level validation: only copies requireApproval when it is a boolean, ignoring any other input - add version guard in tryLoadConfig: warns to stderr when a config file declares a version other than "1.0" so future schema changes can be caught early examples/node9.config.json.example: - add notMatches (&&|\|\||;\s*\S) condition to allow-readonly-bash rule so chained commands like "cat /etc/passwd && rm -rf /" are NOT fast-allowed - tighten review-secrets-write file_path regex: add (^|[/\\]) path separator anchor so "notmy.env" no longer matches — only actual dotenv files do tests (advanced_policy.test.ts): - 6 new tests for allow-readonly-bash: verifies safe commands are allowed, &&/||/; chaining is rejected, pipe-only chains remain allowed - 4 new tests for environments merge: project overrides global, type-unsafe values are dropped, multiple envs merge independently Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ets detection - allow-readonly-bash: add notMatches for $() and backtick to prevent command substitution bypass - allow-install-devtools: add notMatches guard for -g/--global flags - review-secrets-write: change to conditionMode any and add path/filename field checks alongside file_path - Add review-command-substitution rule (catches $() and backtick) - Add review-global-install rule (catches npm/yarn/pnpm -g/--global) - Add tests: $() bypass, backtick bypass, npm install -g, --global, review-secrets-write multi-field (path, filename), version mismatch warning/rejection paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n guard, secrets rule rename - Restore rm and drop to dangerousWords (security regression from previous cleanup) - Fix allow-readonly-bash notMatches: change ;\s*\S to bare ; so a trailing semicolon with no following content cannot bypass the guard - Rename review-secrets-write → flag-secrets-access to reflect that it covers reads as well as writes; update reason string to match - Add .env.bak test: dotfile backup IS flagged, notmy.env.bak is NOT - Update all test fixtures to use ; instead of ;\s*\S to stay consistent with the corrected example config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…movefrom all targets - isNode9Hook now uses word-boundary regex instead of includes() to prevent false matches on binaries that contain 'node9' as a substring (e.g. mynode9) - Add teardownGemini test for legacy double-node hook format - Expand removefrom integration tests to cover all three valid targets (claude, gemini, cursor) not just the unknown-target error path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace ...process.env with a minimal {PATH, NODE9_TESTING} object in
removefrom subprocess invocations so test-runner credentials are not
passed to child processes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add --clear test: HTTP 300 is not a success (2xx boundary is exclusive) - Add teardownClaude test: does not throw on malformed settings.json (readJson already handles parse errors gracefully) - Fix section comment in integration tests: 'removefrom CLI wiring' → 'removefrom command' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
block-force-push, review-git-push, and review-git-destructive all used patterns like `git\s+push` that require the subcommand immediately after `git`. This fails for `git -C <dir> push` which Claude Code (and other agents) use routinely when working across multiple repos. Fix all three patterns to use `git\b.*\b<subcommand>\b` so flags between `git` and the subcommand are absorbed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ed substrings Unanchored patterns like `git\b.*\bpush\b` would match commands such as `echo "git push" && rm -rf /`, triggering a review popup for the wrong reason and potentially masking the actual dangerous operation. Restore `^\s*` anchor on all three rules while keeping `\b.*` to handle `git -C <dir> push` and other flag-before-subcommand forms: - block-force-push - review-git-push - review-git-destructive Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add validateRegex() to core.ts: checks max length (100), balanced
brackets, nested quantifier / alternation / backreference ReDoS vectors
(+ * { only — ? excluded as it is bounded and cannot cause backtracking)
- Add getCompiledRegex() LRU cache (max 500 entries) to avoid repeated
RegExp construction on hot evaluation paths
- Fix matches/notMatches in evaluateSmartConditions to use cache and
fail closed on invalid/dangerous patterns; preserve original null
semantics for notMatches (absent field → condition passes)
- Add scanFilePath() to dlp.ts: resolves symlinks via realpathSync.native
before checking 19 sensitive path patterns (.ssh, .aws, .azure, .kube,
.env, .pem/.key/.p12/.pfx, /etc/passwd|shadow|sudoers, etc.)
- Wire scanFilePath into both DLP check sites in core.ts (path scan runs
before content scan, blocking the read attempt before content is returned)
- Expand DLP_PATTERNS with GCP service account JSON and NPM auth token
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewCode Review: Node9 Proxy — DLP Path Scanning & Regex CacheSecurity Issues (High Priority)1. 2. 3. ReDoS protection has gaps 4. Correctness / Edge Cases5. 6. 7. Type Safety8. SummaryThe file-path DLP concept is sound and the regex caching is a good improvement, but the TOCTOU race, unvalidated Automated review by Claude Sonnet |
Replaces the in-place git approach (which wrote into the user's .git) with an isolated bare repo at ~/.node9/snapshots/<sha256(cwd)[0:16]>/. The user's .git is never touched. Fixes all 6 known flaws of the previous approach: - No lock contention with user's git operations - No untracked file explosion (.git and .node9 hardcoded in info/exclude, plus user's snapshot.ignorePaths passed through) - Works in any directory — .git no longer required - No object store bloat in user's repo - Concurrent sessions use per-invocation GIT_INDEX_FILE inside shadow dir - Auto-recovers from corruption via rev-parse health check + rmSync/reinit Robustness additions: - 15s timeout on all spawnSync calls - Symlink-safe cwd hashing via realpathSync + forward slashes + lowercase on Windows - project-path.txt collision/rename detection; reinitializes on mismatch - core.untrackedCache + core.fsmonitor set on init for performance - Orphaned index file cleanup (files older than 60s left by SIGKILL) - Periodic git gc --auto every 20 snapshots (fire-and-forget) - Legacy fallback in computeUndoDiff/applyUndo for pre-migration hashes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewReview:
|
…h blocking - Replace "Coming Soon" shadow snapshot entry with full implementation details - Document ReDoS protection, LRU regex cache, expanded DLP patterns, and sensitive file path blocking in CHANGELOG [Unreleased] - Expand README shadow snapshots section to mention isolated shadow bare repo and snapshot stack location Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewCode ReviewTest Coverage — Good Progress, Some GapsThe test suite additions are solid overall. The new Gap: The diff appears truncated — the Security Issues
Shadow repo path isolation — the test correctly asserts Correctness / Edge Cases
The truncated test — Minor / Type SafetyThe What Looks Good
Bottom line: One real security gap (
Automated review by Claude Sonnet |
…e specificity, negative GIT_INDEX_FILE assertion - Mock fs.realpathSync.native explicitly alongside the base function; production code calls .native for symlink-escape prevention and the mock was a no-op for that security path - Tighten mockGitSuccess rev-parse check to require --git-dir so other rev-parse variants (e.g. rev-parse HEAD) don't collapse into the health-check branch - Apply the same --git-dir fix to all inline rev-parse mocks across the suite - Add negative assertion: GIT_INDEX_FILE must not contain /.git/ (shadow isolation boundary, not just a positive containment check) - Add spawnResult() helper to reduce repeated `as ReturnType<typeof spawnSync>` casts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewCode Review: Node9 Proxy — Shadow Git Snapshots + ReDoS ProtectionTest Coverage (Primary Focus of This Diff)Test helper
The Missing test cases for new security features:
Security Concerns
(fs.realpathSync as unknown as { native: (p: unknown) => string }).native = vi.fn()...Direct property assignment on the module object can silently fail if the module is frozen or if another import caches the original reference. This is test infrastructure, but if the production symlink-escape check is bypassed in tests, the security guarantee is untested. CHANGELOG mentions Async / Race ConditionsThe Type Safety
DocumentationCHANGELOG is clear and well-written. The "10 snapshots per session" vs "10 snapshots total in SummaryThe test refactoring is directionally good, but three gaps need addressing before merge: (1) the truncated
Automated review by Claude Sonnet |
…lure-path cleanup, symlink assertion
- Add full scanFilePath test suite to dlp.test.ts (was completely untested):
- Sensitive path blocking: .ssh/, .aws/, .env, .env.local, PEM/key files,
/etc/passwd, /etc/shadow
- Negative cases: .envoy and ordinary source files not blocked
- Assert realpathSync.native is called when file exists (symlink-escape path)
- Symlink escape: verify a link resolving to .ssh/id_rsa is blocked
- Safe symlink: verify a link resolving to src/app.ts is not blocked
- Add failure-path index file cleanup test: unlinkSync must fire even when
write-tree returns non-zero (validates the finally block on error paths)
- Add clarifying comment in withShadowRepo: readdirSync → [] is for
cleanOrphanedIndexFiles only; init check uses git rev-parse --git-dir
- Fix README: "per session" → "globally across all sessions" (snapshots.json
is a global file, not scoped to a single session)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewCode Review: Node9 Proxy — Shadow Snapshots, DLP Path Blocking, ReDoS ProtectionCorrectness & Edge Cases
Truncated diff. The diff cuts off mid-sentence at
Security Issues
CHANGELOG claims Race Conditions / Async
TypeScript Type SafetyThe repeated Test Coverage Gaps
MinorThe Overall: The test infrastructure refactor is a meaningful improvement over the previous approach, but the
Automated review by Claude Sonnet |
…s fail-closed, GC unref, symlink isolation Tests added: - validateRegex: accepts valid patterns, rejects empty/long/nested-quantifier/ quantified-alternation patterns, allows safe ? quantifier, rejects bad syntax - getCompiledRegex: returns RegExp, returns null for invalid/ReDoS, cache hit returns same instance, flags treated as distinct cache key - notMatches fail-closed: invalid regex returns false (deny), not true (allow) - notMatches absent-field: null field still returns true (original semantics preserved) - git gc unref(): spawn().unref() is called — prevents blocking Node.js exit - concurrent GIT_INDEX_FILE: both parallel snapshots use shadow-dir paths Bug fixed (undo.ts): - GC condition was `stack.length % 20` checked AFTER splice — stack is capped at MAX_SNAPSHOTS=10 so % 20 never fires; moved check before splice, changed to % 5 Refactors: - dlp.test.ts: extract RealpathWithNative type alias, save/restore .native in afterEach so vi.restoreAllMocks() doesn't leave stale direct property assignment - undo.test.ts: move `import path` to top of file (was after first describe block) - undo.test.ts: import spawn from child_process for unref assertion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewCode Review: Node9 Proxy — ReDoS Protection, DLP Path Blocking, Shadow Git SnapshotsSecurity Issues1. ReDoS detection heuristic is incomplete (medium-high risk) 2. 3. Symlink resolution: Test Coverage Gaps4. 5. No test for LRU eviction behavior 6. No test for concurrent calls to TypeScript / Type Safety7. Minor8. 9. CHANGELOG correctness: "Total built-in patterns: 9" — verify this count is accurate after merging; stale counts in changelogs erode trust. Overall: The security intent is sound, but the ReDoS heuristic coverage and TOCTOU edge case on
Automated review by Claude Sonnet |
…ype consistency
- validateRegex: add reviewer-specific patterns proving heuristic catches
(a{1,10}|b{1,10}){1,10}, (?:a|b){1,100}, (?:a|b)*, (a{2}|b{3})+
- getCompiledRegex: add 520-entry LRU bound test — verifies eviction path runs
without error and all results are valid RegExps (REGEX_CACHE_MAX=500)
- scanFilePath: add two TOCTOU tests — (1) doesn't throw when realpathSync.native
throws due to race between existsSync and native call; (2) sensitive original
path still blocked after native fallback
- DLP pattern count: update stale >= 7 assertion to >= 9 with named list
- Type consistency: add RealpathWithNative alias to undo.test.ts, replace all
`unknown` intermediate casts in both test files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewCode Review: Node9 Proxy — ReDoS Protection, DLP Path Blocking, Shadow Git SnapshotsSecurity Issues (High Priority)1. ReDoS validator may have false negatives 2. TOCTOU window in
...the fallback to 3. LRU cache key collision risk Correctness & Edge Cases4. 5. Test Coverage Gaps6. Shadow repo hash collision / directory rename recovery is untested — the CHANGELOG specifically calls out the 7. 8. 10-snapshot eviction boundary — no test that snapshot 11 correctly evicts snapshot 1 from Minor Issues
SummaryThe ReDoS protection and DLP path-blocking concepts are sound and the test approach is solid. The primary concerns are the TOCTOU fallback behavior (should be fail-closed), the backreference ReDoS gap, and the cache key collision risk. The truncated diff makes it impossible to fully review the shadow snapshot and
Automated review by Claude Sonnet |
…che key collision
Production fix (dlp.ts):
- scanFilePath catch block now returns a block DlpMatch instead of falling back
to path.resolve — a safe-looking symlink pointing to a sensitive file could
exploit the fallback window in a TOCTOU race (existsSync true → native throws)
Tests added (dlp.test.ts):
- TOCTOU fail-closed: verifies block is returned when native() throws, even
when the nominal path looks completely safe (/project/harmless-config.ts)
- Merges previous two TOCTOU tests into one coherent test + adds the attack scenario
Tests added (core.test.ts):
- validateRegex: backreference ReDoS — (\w+)\1+, (\w+)\1*, (\w+)\1{2,} all rejected
- getCompiledRegex: cache key null-byte separator — proves 'foo-i'/'' and 'foo-'/i
get distinct cache entries (no collision despite i appearing in both)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewCode Review: Node9 Proxy PRSecurity Issues (Critical Path)
Symlink resolution scope ReDoS pattern: Async/Race Conditions
Type Safety
Test Coverage Gaps
Minor
Overall the security intent is sound, but the TOCTOU and symlink mock fidelity issues need verification before merge.
Automated review by Claude Sonnet |
…validate flags dlp.ts — scanFilePath: - Remove existsSync pre-check; call realpathSync.native() unconditionally - Eliminates the race window between existsSync and native() - ENOENT/ENOTDIR → file doesn't exist → fall back to path.resolve (safe) - Any other error → fail-closed (block) core.ts — validateRegex: - Relax quantified-alternation check: only reject when alternatives themselves contain quantifiers (e.g. (a+|b+)* is dangerous; (GET|POST)+ is safe) - Legitimate user patterns like (https?|ftp):// and (GET|POST)+ now pass core.ts — getCompiledRegex: - Validate flags against /^[gimsuy]+$/ before compilation - Returns null for invalid flags (e.g. 'z') instead of throwing Tests: - dlp.test.ts: remove existsSync mocks from symlink tests, add path traversal test (../../.ssh/id_rsa), ENOENT-as-safe test, EACCES fail-closed test - core.test.ts: safe alternations now pass, dangerous ones still blocked, invalid/valid flags tests for getCompiledRegex Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Claude Code ReviewCode Review: Node9 Proxy — ReDoS Protection, DLP Expansion, Shadow Snapshots1. Security:
|
Auto-generated PR
Merge latest
devchanges intomainto trigger a release.