Skip to content

Implement DiffVouch as a standalone Go CLI - #4

Merged
divyangchauhan merged 10 commits into
mainfrom
feat/diffvouch-go-cli
Sep 8, 2026
Merged

divyangchauhan merged 10 commits into
mainfrom
feat/diffvouch-go-cli

Conversation

@divyangchauhan

Copy link
Copy Markdown
Owner

Summary

  • replace the planned runtime-dependent CLI with a standalone Go implementation named diffvouch
  • review working-tree, staged, committed branch, and GitHub PR diffs through Codex/Claude subscriptions or explicitly selected OpenAI/Anthropic APIs
  • calculate deterministic ratings out of 5, separate blocking and non-blocking findings, enforce optional quality gates, and emit terminal or versioned JSON output
  • publish one comment-only GitHub review through a private app owned by the user or repository organization, including valid inline comments on added and deleted lines
  • add secure local credential handling, trusted repository configuration, bounded/chunked diff collection, prompt isolation, and secret redaction
  • package native macOS, Linux, and Windows binaries for AMD64 and ARM64 with GoReleaser
  • update the PRD and README for the implemented CLI and retain the portable Agent Skill installation path

This Go CLI supersedes the Python CLI approach in #3; that PR is left unchanged for reference.

Security and publication behavior

  • API transport is opt-in and never selected by repository configuration
  • repository-local config is loaded from the trusted base commit
  • OS credential storage is preferred, with a mode-0600 fallback
  • GitHub App private keys and provider API keys are never stored in the repository
  • installation tokens are repository-restricted, held only in memory, and regenerated on demand
  • publishing revalidates the PR base and head SHAs and always uses the COMMENT event

Validation

  • go test -race ./...
  • go vet ./...
  • git diff --check
  • standalone binary end-to-end review through the Codex subscription using gpt-5.6-sol at high effort
  • GoReleaser snapshot produced checksummed archives for:
    • Linux AMD64 and ARM64
    • macOS AMD64 and ARM64
    • Windows AMD64 and ARM64

@diffvouch diffvouch 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.

DiffVouch review

Rating: 2.5/5 — Needs work

The implementation has strong bounded-diff and structured-output foundations, but it should not merge before untrusted filenames are safely rendered and publication performs its final SHA validation after preparing the review. Runtime disclosure of disabled redaction, early gate validation, and rating boundary corrections are also needed.

Blocking issues

  • high · Escape untrusted filenames before displaying them internal/gitdiff/gitdiff.go:354
    A repository can use filenames containing newline, ESC, or other terminal-control characters to forge output or emit terminal escape sequences when DiffVouch reports skipped/excluded files or finding locations. Treating repository paths as untrusted requires escaping control characters at presentation boundaries.
    Recommendation: Retain raw paths internally, but quote or escape control characters before rendering them to terminals, logs, errors, or review Markdown. Add tests using newline and ESC-containing filenames.
  • medium · Revalidate the PR after preparing publication internal/github/github.go:758
    The PR can be updated after the validation but before review creation. Because the request explicitly supplies the previously reviewed commit_id, this can publish a review for stale code despite the advertised base/head safety check.
    Recommendation: Fetch and process the PR diff first, then re-fetch and validate the live base/head immediately before creating the review. Where supported, also use an API precondition to narrow the remaining race.

Non-blocking issues

  • medium · Warn at runtime before sending an unredacted patch internal/review/review.go:151
    Users who have not read the documentation receive no warning that credentials in the patch will be transmitted unchanged. This contradicts the stated requirement to warn while redaction is disabled and can lead to accidental secret disclosure.
    Recommendation: Emit a prominent warning before the first provider call whenever unredacted patch submission is enabled, including for JSON/output-file workflows through an appropriate diagnostics channel.
  • medium · Validate quality-gate severity before provider calls internal/review/review.go:128
    A typo in --fail-on-severity, or an invalid configured threshold, can incur API cost and spend several minutes reviewing before the command discards the result with an argument error.
    Recommendation: Validate both CLI and repository-configured severity thresholds before collecting the patch or invoking a provider.
  • medium · Align rating labels with the documented ranges internal/rating/rating.go:34
    The documented ranges define 1.5–2.4 as High risk and 2.5–3.4 as Needs work. Consequently, scores from 2.0 through 2.4 are mislabeled; a critical finding capped at 1.5 is also labeled correctly only by coincidence with the lower branch.
    Recommendation: Change the High risk upper threshold to 2.5 and retain Needs work for scores from 2.5 through 3.4. Add boundary tests for 1.4, 1.5, 2.4, 2.5, 3.4, and 3.5.

Needs verification

  • Verify whether the GitHub review endpoint offers a suitable conditional-request mechanism for making publication validation stronger than a final pre-POST refetch.

Rating breakdown

Dimension Score
Correctness 3.2/5
Security 2.6/5
Maintainability 4.0/5
Testing 3.4/5
Scope 4.5/5

Provider: codex/cli · Model: gpt-5.6-sol · Reviewed commit: dd3d61a904876e9bf8f20ecf9d48e50181272a3f

Comment thread internal/gitdiff/gitdiff.go Resolved
Comment thread internal/github/github.go Resolved
Comment thread internal/review/review.go Resolved
Comment thread internal/review/review.go Resolved
Comment thread internal/rating/rating.go Resolved

@diffvouch diffvouch 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.

DiffVouch review

Rating: 2.5/5 — Needs work

Reviewed the complete patch in 2 chunks. Chunk 1 has strong fail-closed collection and publication checks, but it contains two blocking trust-boundary issues: Claude is launched from the untrusted repository, and arbitrary provider text is rendered directly to terminals. Oversized patches also have an edge-case chunk-corruption defect. The fallback store has a blocking lost-update race because its read-modify-write operations are not serialized. It also panics when the fallback file contains valid JSON null.

Blocking issues

  • high · Claude reviews are launched inside the untrusted repository internal/provider/provider.go:152
    The Claude review therefore starts in the invoking repository. Claude Code can load repository-scoped instructions, settings, and lifecycle hooks before reviewing the supplied patch. A malicious change can consequently influence the supposedly trusted review context or cause repository-configured hook commands to run, violating the stated isolation and no-repository-code-execution guarantees.
    Recommendation: Run Claude from a newly created empty temporary directory and explicitly disable project/local setting sources, hooks, plugins, and MCP configuration. Add a test using a repository-local configuration or startup hook to verify it is neither loaded nor executed.
  • high · Untrusted provider text reaches the terminal without escaping internal/render/render.go:22
    Provider responses are untrusted even after schema validation. A response containing ANSI/OSC terminal sequences can manipulate terminal output, forge subsequent diagnostics, create misleading hyperlinks, or modify the clipboard on supporting terminals. Only file paths currently receive terminal-safe escaping.
    Recommendation: Apply a terminal-specific sanitizer to every provider-controlled string before rendering human-readable output, escaping control and formatting characters while leaving JSON output unchanged. Cover summaries, all finding fields, observations, and verification notes with tests containing CSI and OSC sequences.
  • high · Concurrent fallback updates can silently lose secrets internal/secret/store.go:54
    Concurrent Store/Delete calls can read the same snapshot and then overwrite each other. For example, two successful stores of different names can leave only the last writer's value, even though both calls returned success. This can silently lose credentials.
    Recommendation: Serialize the complete read-modify-write transaction with a lock that works across processes, then atomically replace the file while holding that lock.

Non-blocking issues

  • medium · Chunk splitting can mistake filename text for a hunk header internal/gitdiff/gitdiff.go:397
    A valid filename or other header value containing @@ can be selected as the hunk boundary. For a file exceeding the chunk limit, this truncates the repeated file header and sends malformed chunks to the provider while the final result is still labeled complete. Findings may then be misattributed or discarded during location validation.
    Recommendation: Locate the first hunk with the same line-anchored expression used for subsequent hunks, such as (?m)^@@ , and add an oversized-file test whose quoted filename contains @@ .
  • low · JSON null fallback file causes a panic internal/secret/store.go:54
    A syntactically valid secrets.json containing null crashes Store instead of returning an error or recovering with an empty store.
    Recommendation: After unmarshalling, reject a nil map as invalid or initialize it with map[string]string{} before assignment.

Needs verification

  • Review patch chunk 2 before making an overall merge decision; omitted implementation was not assessed here.

Rating breakdown

Dimension Score
Correctness 3.1/5
Security 2.0/5
Maintainability 3.7/5
Testing 3.4/5
Scope 4.4/5

Provider: codex/cli · Model: gpt-5.6-sol · Reviewed commit: 9bc7d264494735b3843ddf995d4245f51c42cd38

args = append(args, "--effort", a.effort)
}
args = append(args, "-p")
command := exec.CommandContext(ctx, "claude", args...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude reviews are launched inside the untrusted repository

The Claude review therefore starts in the invoking repository. Claude Code can load repository-scoped instructions, settings, and lifecycle hooks before reviewing the supplied patch. A malicious change can consequently influence the supposedly trusted review context or cause repository-configured hook commands to run, violating the stated isolation and no-repository-code-execution guarantees.

Recommendation: Run Claude from a newly created empty temporary directory and explicitly disable project/local setting sources, hooks, plugins, and MCP configuration. Add a test using a repository-local configuration or startup hook to verify it is neither loaded nor executed.

Comment thread internal/render/render.go Outdated

func Terminal(result model.ReviewResult) string {
var output strings.Builder
fmt.Fprintf(&output, "DiffVouch review: %.1f/5 — %s\n\n%s\n\n", result.Rating.Overall, result.Rating.Label, result.Summary)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Untrusted provider text reaches the terminal without escaping

Provider responses are untrusted even after schema validation. A response containing ANSI/OSC terminal sequences can manipulate terminal output, forge subsequent diagnostics, create misleading hyperlinks, or modify the clipboard on supporting terminals. Only file paths currently receive terminal-safe escaping.

Recommendation: Apply a terminal-specific sanitizer to every provider-controlled string before rendering human-readable output, escaping control and formatting characters while leaving JSON output unchanged. Cover summaries, all finding fields, observations, and verification notes with tests containing CSI and OSC sequences.

Comment thread internal/gitdiff/gitdiff.go Outdated
current.WriteString(section)
continue
}
hunkAt := strings.Index(section, "@@ ")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Chunk splitting can mistake filename text for a hunk header

A valid filename or other header value containing @@ can be selected as the hunk boundary. For a file exceeding the chunk limit, this truncates the repeated file header and sends malformed chunks to the provider while the final result is still labeled complete. Findings may then be misattributed or discarded during location validation.

Recommendation: Locate the first hunk with the same line-anchored expression used for subsequent hunks, such as (?m)^@@ , and add an oversized-file test whose quoted filename contains @@ .

Comment thread internal/secret/store.go Outdated
values, err := loadFile()
if err != nil {
return Ref{}, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Concurrent fallback updates can silently lose secrets

Concurrent Store/Delete calls can read the same snapshot and then overwrite each other. For example, two successful stores of different names can leave only the last writer's value, even though both calls returned success. This can silently lose credentials.

Recommendation: Serialize the complete read-modify-write transaction with a lock that works across processes, then atomically replace the file while holding that lock.

Comment thread internal/secret/store.go Outdated
values, err := loadFile()
if err != nil {
return Ref{}, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

JSON null fallback file causes a panic

A syntactically valid secrets.json containing null crashes Store instead of returning an error or recovering with an empty store.

Recommendation: After unmarshalling, reject a nil map as invalid or initialize it with map[string]string{} before assignment.

@diffvouch diffvouch 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.

DiffVouch review

Rating: 2.5/5 — Needs work

Reviewed the complete patch in 2 chunks. Chunk 1 introduces a substantial, well-tested CLI foundation, but the Claude adapter lacks filesystem isolation and GitHub publication trusts provider-generated Markdown. Large GitHub diffs can also be silently truncated, and concurrent credential configuration is not safely serialized. No actionable issue is evidenced by this patch chunk. The secret-store implementation has sensible backend validation, locking, error handling, and focused regression coverage.

Blocking issues

  • high · Claude reviewer runs inside the untrusted repository internal/provider/provider.go:152
    Claude Code can discover project-level instructions, settings, or hooks from its working directory. Running it inside the reviewed repository undermines the promised isolation and may allow repository-controlled configuration to influence the review or execute during CLI startup.
    Recommendation: Run Claude from a newly created empty temporary directory and explicitly disable project/local settings, hooks, and automatic repository discovery using supported Claude CLI controls.
  • medium · Untrusted provider text is published as raw GitHub Markdown internal/github/github.go:803
    Provider output is derived from adversarial patch text and must remain untrusted. Markdown such as mentions, links, HTML, or forged headings can therefore be published by the configured bot, potentially notifying users or making the review misleading.
    Recommendation: Escape or neutralize GitHub Markdown and mentions in all provider-controlled fields before constructing overall and inline review bodies. Preserve only deliberately supported formatting.

Non-blocking issues

  • medium · Large GitHub diffs are silently truncated during publication internal/github/github.go:512
    A pull-request diff larger than 8 MiB is silently truncated. Publish treats that partial diff as complete, drops inline findings whose lines occur after the cutoff, and can still report successful publication.
    Recommendation: Read up to 8<<20 + 1 bytes and fail closed when the extra byte is present, or use a complete paginated/file-based mechanism for determining commentable lines.
  • medium · Concurrent configuration writes can corrupt or lose credentials internal/privatefile/privatefile.go:15
    Two simultaneous authentication or GitHub configuration commands can truncate and write the same temporary inode, encounter rename races, or overwrite each other's independently updated configuration. This can corrupt the JSON file or lose credential references.
    Recommendation: Serialize the entire global-config read/modify/write transaction with a cross-process lock and use a uniquely created temporary file in the destination directory before atomic replacement.
  • low · API-transport validation test passes for the wrong reason internal/config/config_test.go:70
    The assertion passes because the trusted commit does not contain the file, not because default_transport: api reaches and fails configuration validation. Removing the API-transport validation could leave this test green.
    Recommendation: Commit unsafe.yml before loading it, or place it outside the repository so parsing reaches validate, and assert that the returned error specifically identifies API transport.

Needs verification

  • Confirm whether every supported Claude CLI version's --safe-mode disables project instructions, settings, and startup hooks; the adapter does not otherwise isolate its working directory.
  • The supplied chunk relies on internal/privatefile.Write for secure permissions and write behavior, but that implementation was not included in this chunk.

Rating breakdown

Dimension Score
Correctness 3.4/5
Security 2.7/5
Maintainability 3.5/5
Testing 3.6/5
Scope 3.9/5

Provider: codex/cli · Model: gpt-5.6-sol · Reviewed commit: 581ecc227138b57d496fe035ca239e0c2a5f6afa

args = append(args, "--effort", a.effort)
}
args = append(args, "-p")
command := exec.CommandContext(ctx, "claude", args...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium confidence: Claude reviewer runs inside the untrusted repository

Claude Code can discover project-level instructions, settings, or hooks from its working directory. Running it inside the reviewed repository undermines the promised isolation and may allow repository-controlled configuration to influence the review or execute during CLI startup.

Recommendation: Run Claude from a newly created empty temporary directory and explicitly disable project/local settings, hooks, and automatic repository discovery using supported Claude CLI controls.

Comment thread internal/github/github.go Outdated
}
locationText += "`"
}
fmt.Fprintf(&output, "- **%s · %s**%s\n %s\n **Recommendation:** %s\n", finding.Severity, finding.Title, locationText, finding.Explanation, finding.Recommendation)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Untrusted provider text is published as raw GitHub Markdown

Provider output is derived from adversarial patch text and must remain untrusted. Markdown such as mentions, links, HTML, or forged headings can therefore be published by the configured bot, potentially notifying users or making the review misleading.

Recommendation: Escape or neutralize GitHub Markdown and mentions in all provider-controlled fields before constructing overall and inline review bodies. Preserve only deliberately supported formatting.

Comment thread internal/github/github.go
if response.StatusCode < 200 || response.StatusCode >= 300 {
detail := string(raw)
if len(detail) > 4096 {
detail = detail[:4096]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Large GitHub diffs are silently truncated during publication

A pull-request diff larger than 8 MiB is silently truncated. Publish treats that partial diff as complete, drops inline findings whose lines occur after the cutoff, and can still report successful publication.

Recommendation: Read up to 8<<20 + 1 bytes and fail closed when the extra byte is present, or use a complete paginated/file-based mechanism for determining commentable lines.

Comment thread internal/privatefile/privatefile.go Outdated
return err
}
_ = os.Chmod(filepath.Dir(path), 0o700)
temporary := path + ".tmp"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Concurrent configuration writes can corrupt or lose credentials

Two simultaneous authentication or GitHub configuration commands can truncate and write the same temporary inode, encounter rename races, or overwrite each other's independently updated configuration. This can corrupt the JSON file or lose credential references.

Recommendation: Serialize the entire global-config read/modify/write transaction with a cross-process lock and use a uniquely created temporary file in the destination directory before atomic replacement.

_ = os.WriteFile(path, []byte("version: 1\nprovider:\n default_transport: api\n"), 0o600)
if _, err := LoadRepository(repo, path, "HEAD"); err == nil {
t.Fatal("API transport should not be selectable by repository config")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

API-transport validation test passes for the wrong reason

The assertion passes because the trusted commit does not contain the file, not because default_transport: api reaches and fails configuration validation. Removing the API-transport validation could leave this test green.

Recommendation: Commit unsafe.yml before loading it, or place it outside the repository so parsing reaches validate, and assert that the returned error specifically identifies API transport.

@divyangchauhan
divyangchauhan merged commit 119d547 into main Sep 8, 2026
7 checks passed
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