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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Security

- **`--help` no longer prints secrets from the environment.** clap shows `[env: NAME=value]` for options bound to an env var, so `cora --help` with `CORA_API_KEY` or `GITHUB_TOKEN` set printed the key (into logs, CI output, screenshots). `--api-key` and `--token` now hide their env value.

### Changed

- **Help points to agent mode.** `cora --help`, `cora review/scan --help`, `cora mcp --help` and `cora install --help` now mention `--no-llm`, the agent-as-reviewer MCP tools, the `/cora-review` skill and `claude mcp add cora -- cora mcp`.

## [0.19.0] - 2026-10-16

### Added
Expand Down
2 changes: 2 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Complete command reference for the cora CLI.
| `cora install --dry-run` | Show what would be changed |
| `cora install --force` | Overwrite existing cora entry |
| `cora install --yes` | Install ALL detected agents (non-interactive) |
| `cora mcp` | Start the MCP server (22 tools, incl. agent-as-reviewer: `cora.review_prepare`/`review_finalize`, `cora.scan_prepare`/`scan_finalize`; no LLM key needed in cora). See [Review with your agent](./usage#review-with-your-agent-no-llm-setup) |
| `claude mcp add cora -- cora mcp` | Register the MCP server with Claude Code (`cora install` only adds its `/cora-review` skill) |
| `cora hook install` | Install pre-commit hook |
| `cora hook uninstall` | Remove pre-commit hook |
| `cora completion` `<shell>` | Generate shell completions (bash/zsh/fish/powershell) |
Expand Down
30 changes: 27 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ use formatters::OutputFormat;
///
/// Review diffs and scan projects using any OpenAI-compatible LLM.
/// Configure via .cora.yaml, CLI flags, or environment variables.
///
/// No API key? `cora review --no-llm` / `cora scan --no-llm` run the
/// deterministic checks only, and an AI coding agent can do the AI review
/// itself through the MCP server (`cora mcp`, see `cora mcp --help`).
#[derive(Parser, Debug)]
#[clap(
name = "cora",
Expand Down Expand Up @@ -74,7 +78,7 @@ struct GlobalOptions {
pub base_url: Option<String>,

/// API key (or set `CORA_API_KEY` env var for CI, or use `cora auth login`)
#[clap(long, global = true, env = "CORA_API_KEY")]
#[clap(long, global = true, env = "CORA_API_KEY", hide_env_values = true)]
pub api_key: Option<String>,

/// Enable verbose logging
Expand Down Expand Up @@ -246,6 +250,10 @@ enum Command {
},

/// Review a git diff (staged, unpushed, or branch)
///
/// Needs an LLM (BYOK). Without an API key, or with --no-llm, only the
/// deterministic checks run. To review with your AI coding agent instead
/// (no key in cora), see `cora mcp --help`.
Review {
/// Review staged changes (git diff --cached)
#[clap(long)]
Expand Down Expand Up @@ -321,7 +329,7 @@ enum Command {
ref_name: Option<String>,

/// GitHub token for upload (default: `GITHUB_TOKEN` env var)
#[clap(long, env = "GITHUB_TOKEN")]
#[clap(long, env = "GITHUB_TOKEN", hide_env_values = true)]
token: Option<String>,

/// Recall project patterns from Uteke before review (requires `uteke` CLI)
Expand All @@ -339,6 +347,10 @@ enum Command {
},

/// Scan a project directory for issues
///
/// Needs an LLM (BYOK). Without an API key, or with --no-llm, only the
/// deterministic checks run. To scan with your AI coding agent instead
/// (no key in cora), see `cora mcp --help`.
Scan {
/// Root directory to scan (default: current directory)
#[clap(long)]
Expand Down Expand Up @@ -400,7 +412,7 @@ enum Command {
ref_name: Option<String>,

/// GitHub token (default: `GITHUB_TOKEN` env var)
#[clap(long, env = "GITHUB_TOKEN")]
#[clap(long, env = "GITHUB_TOKEN", hide_env_values = true)]
token: Option<String>,
},

Expand Down Expand Up @@ -443,6 +455,14 @@ enum Command {
},

/// Start MCP server (Model Context Protocol for AI agents)
///
/// Includes agent-as-reviewer tools: `cora.review_prepare` +
/// `cora.review_finalize` (a diff) and `cora.scan_prepare` +
/// `cora.scan_finalize` (a project). The calling agent judges with its own
/// model, so no LLM or API key is configured in cora. Register the server
/// with your agent via `cora install` (for Claude Code:
/// `claude mcp add cora -- cora mcp`). Results are advisory, not a CI
/// gate. See docs/usage.md, "Review with your agent".
Mcp,

/// Manage review findings (list, dismiss, reopen, stats)
Expand Down Expand Up @@ -490,6 +510,10 @@ enum Command {
filter: Option<String>,
},
/// Auto-detect and configure AI coding agents for Cora MCP
///
/// Also installs the /cora-review skill for Claude Code
/// (~/.claude/skills/cora-review). Claude Code itself is registered with
/// `claude mcp add cora -- cora mcp`.
Install {
/// List detected agents without installing
#[clap(long)]
Expand Down
56 changes: 56 additions & 0 deletions tests/cli_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,59 @@ fn init_without_force_fails_if_exists() {
.failure()
.stderr(predicate::str::contains("already exists"));
}

// ═══════════════════════════════════════════════════════════
// Help points to agent mode, and never prints secrets from the environment
// ═══════════════════════════════════════════════════════════

#[test]
fn cli_mcp_help_explains_agent_as_reviewer() {
cora_cmd()
.args(["mcp", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("agent-as-reviewer"))
.stdout(predicate::str::contains("cora.review_prepare"))
.stdout(predicate::str::contains("cora.scan_finalize"))
.stdout(predicate::str::contains("claude mcp add cora"));
}

#[test]
fn cli_install_help_mentions_the_skill() {
cora_cmd()
.args(["install", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("/cora-review"));
}

#[test]
fn cli_review_and_scan_help_point_to_no_llm_and_agent_mode() {
for cmd in ["review", "scan"] {
cora_cmd()
.args([cmd, "--help"])
.assert()
.success()
.stdout(predicate::str::contains("--no-llm"))
.stdout(predicate::str::contains("cora mcp --help"));
}
}

#[test]
fn cli_help_does_not_print_secret_env_values() {
// clap prints `[env: NAME=value]` by default; a key in the environment
// must never end up in help output (logs, screenshots, CI).
for args in [
&["--help"][..],
&["review", "--help"],
&["upload-sarif", "--help"],
] {
cora_cmd()
.args(args)
.env("CORA_API_KEY", "sk-secret-do-not-print-123")
.env("GITHUB_TOKEN", "ghp_secret_do_not_print_456")
.assert()
.stdout(predicate::str::contains("sk-secret-do-not-print-123").not())
.stdout(predicate::str::contains("ghp_secret_do_not_print_456").not());
}
}
Loading