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
25 changes: 25 additions & 0 deletions .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/cursor-marketplace.json",
"name": "rogue-marketplace",
"version": "1.0.0",
"description": "Rogue Security extensions for Cursor",
"owner": {
"name": "Rogue Security",
"email": "support@rogue.security",
"url": "https://www.rogue.security"
},
"plugins": [
{
"name": "rogue-security",
"version": "1.0.3",
"description": "Rogue Security AIDR — real-time AI agent detection and response for Cursor",
"author": {
"name": "Rogue Security",
"url": "https://www.rogue.security"
},
"homepage": "https://docs.rogue.security/integrations/cursor",
"category": "security",
"source": "./plugins/cursor"
}
]
}
72 changes: 72 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Validate

# Catches the failure mode release.yml can't: a plugin's manifest version drifting
# out of sync with its marketplace entry, or a malformed manifest/hooks file landing
# on main. Runs on every push/PR — fast, no secrets, no release side effects.
# (Ported from qualifire-dev/rogue-plugin-cursor's release-time version check and
# generalized to all three plugins in this monorepo.)

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
manifests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Validate JSON is well-formed
run: |
set -euo pipefail
fail=0
while IFS= read -r f; do
if ! jq empty "$f" 2>/dev/null; then
echo "::error file=$f::invalid JSON"
fail=1
fi
done < <(git ls-files '*.json')
[ "$fail" = 0 ] || exit 1

- name: Plugin version <-> marketplace version in sync
run: |
set -euo pipefail
# plugin_manifest | marketplace_manifest | source-path key
rows="
plugins/rogue/.claude-plugin/plugin.json|.claude-plugin/marketplace.json|./plugins/rogue
plugins/codex/.codex-plugin/plugin.json|.agents/plugins/marketplace.json|./plugins/codex
plugins/cursor/.cursor-plugin/plugin.json|.cursor-plugin/marketplace.json|./plugins/cursor
"
fail=0
echo "$rows" | while IFS='|' read -r plugin market src; do
[ -n "${plugin:-}" ] || continue
[ -f "$plugin" ] || { echo "::error::missing $plugin"; exit 1; }
[ -f "$market" ] || { echo "::error::missing $market"; exit 1; }
pv=$(jq -r '.version' "$plugin")
mv=$(jq -r --arg s "$src" '.plugins[] | select(.source == $s) | .version' "$market")
if [ -z "$mv" ] || [ "$mv" = "null" ]; then
echo "::error file=$market::no plugin entry with source $src"; exit 1
fi
if [ "$pv" != "$mv" ]; then
echo "::error::version drift — $plugin ($pv) != $market entry $src ($mv)"; exit 1
fi
echo "ok: $src @ $pv"
done

- name: Shell scripts parse
run: |
set -euo pipefail
# Parse each script with the interpreter its shebang names: the runtime
# hook dispatchers are POSIX sh (validate under dash), while install.sh /
# build-release.sh are bash (arrays, RETURN traps). Wrong parser = false fail.
fail=0
while IFS= read -r f; do
if head -1 "$f" | grep -q bash; then chk="bash -n"; else chk="dash -n"; fi
if ! $chk "$f"; then echo "::error file=$f::$chk parse error"; fail=1; fi
done < <(git ls-files 'plugins/**/scripts/*.sh' 'install.sh' 'scripts/*.sh')
[ "$fail" = 0 ] || exit 1
13 changes: 10 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ There is no build step for the plugin itself: it's a directory of JSON + shell s

**Cross-platform by dual dispatcher.** Every event ships TWO implementations — a POSIX-`sh` script (`hook.sh` & friends) for macOS/Linux/WSL and a PowerShell sibling (`hook.ps1` & friends) for **native Windows (no WSL, no Git Bash)**. `hooks.json` registers an `sh` entry and a PowerShell entry for each event; exactly one does real work per machine (see "The hook pattern"). When you change one dispatcher's behavior, change the other to match — keep `hook.sh` / `hook.ps1` in lockstep.

**This repo is now a multi-agent monorepo.** Besides the Claude plugin (`plugins/rogue/`, endpoint `/hooks/claude`) it also ships the **OpenAI Codex** plugin (`plugins/codex/`, endpoint `/hooks/openai`, family `openai`, surface `codex_cli`/`codex_app`). The one-line installer (`install.sh` / `install.ps1`) detects **every** supported agent on PATH (`claude`, `codex`) and installs the matching Rogue plugin into each, writing the shared `~/.rogue-env` once.
**This repo is now a multi-agent monorepo.** Besides the Claude plugin (`plugins/rogue/`, endpoint `/hooks/claude`) it also ships the **OpenAI Codex** plugin (`plugins/codex/`, endpoint `/hooks/openai`, family `openai`, surface `codex_cli`/`codex_app`) and the **Cursor** plugin (`plugins/cursor/`, endpoint `/hooks/cursor`, header `x-rogue-source: cursor`). The one-line installer (`install.sh` / `install.ps1`) detects **every** supported agent (`claude`, `codex`, `cursor`) and installs the matching Rogue plugin into each, writing the shared `~/.rogue-env` once.

### Codex plugin (`plugins/codex/`)
Mirrors the Claude plugin with deliberate differences:
Expand All @@ -21,12 +21,19 @@ Mirrors the Claude plugin with deliberate differences:
- No `CLAUDE_CODE_ENTRYPOINT` gate (Codex doesn't set it; the hook only ever fires from Codex's own `hooks.json`).
- **Hook trust**: Codex hashes the whole hook definition and skips untrusted command hooks until reviewed via `/hooks`. Keep `hooks.json` command strings (POSIX `command` + Windows `commandWindows`) **byte-identical forever**; mutate only `scripts/*` so trust survives updates. Setup/status commands document the one-time `/hooks` trust step.

### Cursor plugin (`plugins/cursor/`)
A near-verbatim port of `qualifire-dev/rogue-plugin-cursor`'s `plugins/rogue/` (keep it in sync — re-pull on upstream changes). Mirrors the Claude/Codex dual-dispatcher with Cursor-native wiring:
- **Dual dispatcher (sh + PowerShell), PURE RELAY.** Each of the 18 Cursor events registers two `hooks.json` entries — `sh ./scripts/hook.sh <event>` (cwd-relative; Cursor runs hooks from the plugin root) and a PowerShell entry that loads `scripts/hook.ps1` via `$env:CURSOR_PLUGIN_ROOT`. Exactly one runs per machine (same arbitration as Claude). Endpoint `/api/v1/hooks/cursor`, header `x-rogue-source: cursor`, env var `CURSOR_PLUGIN_ROOT`. Reuses the shared `~/.rogue-env`. `setup.sh` / `setup.ps1` write it.
- **Manifest is `.cursor-plugin/plugin.json`** (version is source of truth); the Cursor marketplace file is the repo-root `.cursor-plugin/marketplace.json` (source `./plugins/cursor`, plugin version must match plugin.json — enforced by `.github/workflows/validate.yml`), kept separate from `.claude-plugin/` and `.agents/plugins/`.
- **No `auto-update.sh`.** The Cursor **Team Marketplace** (admin imports the repo via Dashboard) IS Cursor's native managed/auto-update path — we don't ship a script. Per-developer one-liner installs upgrade by re-running the installer.
- **`commands/{setup,status}.md`**, not `skills/` — Cursor's slash-command format.

### Multi-agent install (`install.sh` / `install.ps1`)
The single one-line installer detects every supported agent on PATH (`have_cmd claude` / `have_cmd codex`; PowerShell uses `Get-Command`), writes the shared `~/.rogue-env` **once** (`configure_credentials`), then runs each agent's marketplace+plugin install (`claude plugin install` / `codex plugin install rogue@rogue-marketplace` against the same monorepo). Adding an agent = one detect line + one `*_install_plugin` function. Codex prints the one-time `/hooks` trust reminder.
The single one-line installer detects every supported agent (`have_cmd claude` / `have_cmd codex` / `have_cmd cursor || [ -d ~/.cursor ]`; PowerShell uses `Get-Command` / `Test-Path`), writes the shared `~/.rogue-env` **once** (`configure_credentials`), then installs each. **Claude and Codex use their native plugin CLIs** (`claude plugin install` / `codex plugin add rogue@rogue-marketplace` against the same monorepo — git-clones the marketplace, no local files). **Cursor has NO plugin CLI** — `cursor_install_plugin` downloads the release tarball (`rogue-plugin-cursor.tar.gz`) and copies `plugins/cursor/` into `~/.cursor/plugins/local/rogue` (`%USERPROFILE%\.cursor\plugins\local\rogue` on Windows). This copy-vs-CLI asymmetry is load-bearing — preserve it. Adding a CLI agent = one detect line + one `*_install_plugin` function; Codex prints the one-time `/hooks` trust reminder, Cursor warns if `python3` is absent.

## Repo layout (load-bearing pieces)

- `.claude-plugin/marketplace.json` — marketplace manifest. Points at `./plugins/rogue`.
- `.claude-plugin/marketplace.json` — Claude marketplace manifest. Points at `./plugins/rogue`. (`.agents/plugins/marketplace.json` → `./plugins/codex`; `.cursor-plugin/marketplace.json` → `./plugins/cursor`, the Cursor Team-Marketplace import target.)
- `plugins/rogue/.claude-plugin/plugin.json` — plugin manifest. **`version` here is the source of truth** — `build-release.sh` reads it, and `auto-update.sh` compares it against the latest GitHub release tag (`v${version}`).
- `plugins/rogue/hooks/hooks.json` — 11 lifecycle hooks, all `type: "command"`. **Every event registers two entries** (an `sh` one and a PowerShell one) — see below.
- `plugins/rogue/scripts/hook.sh` — POSIX-`sh` + `curl` dispatcher (macOS/Linux/WSL). Invoked via `sh` (NOT `bash`), so it is kept POSIX-clean (tested under `dash` via `tests/test_hook_sh.sh`). **Stands down** (emits `{}`, exits) under Git Bash (`uname` = MINGW/MSYS/CYGWIN) so the PowerShell entry owns native Windows.
Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,25 @@ Pass credentials via environment variables before the one-liner when running non
$env:ROGUE_API_KEY='rsk_xxx'; $env:ROGUE_ACTOR_EMAIL='you@co.com'; iwr -useb https://raw.githubusercontent.com/qualifire-dev/rogue-plugins/main/install.ps1 | iex
```

The installer adds the marketplace and installs the plugin via the Claude CLI
(`claude plugin marketplace add` + `claude plugin install`), validates and writes
your API key to `~/.rogue-env` (`%USERPROFILE%\.rogue-env` on Windows), and
confirms your actor identity. On macOS/Linux it also configures a `Rogue Security`
status badge below the prompt (🟢 connected / 🔴 not set up).
The one installer detects every supported coding agent and installs the matching
Rogue plugin into each — **Claude Code**, **OpenAI Codex**, and **Cursor** — writing
the shared `~/.rogue-env` (`%USERPROFILE%\.rogue-env` on Windows) once. Claude and
Codex install through their native plugin CLIs (`claude plugin install` /
`codex plugin add`); **Cursor has no plugin CLI**, so its plugin is copied into
`~/.cursor/plugins/local/rogue` from the release tarball. The installer validates and
writes your API key, confirms your actor identity, and on macOS/Linux configures a
`Rogue Security` status badge below the Claude prompt (🟢 connected / 🔴 not set up).

To target specific agents instead of all detected ones, pass `--claude`, `--codex`,
and/or `--cursor` (PowerShell: `-Claude` / `-Codex` / `-Cursor`):

```bash
curl -fsSL https://raw.githubusercontent.com/qualifire-dev/rogue-plugins/main/install.sh | bash -s -- --cursor
```

For org-wide Cursor rollout, import this repo as a Cursor **Team Marketplace**
(Dashboard → marketplaces) — that is Cursor's native managed/auto-update path, separate
from the per-developer one-liner above.

Native Windows support requires no WSL or Git Bash: every hook ships both a POSIX
`sh` script and a PowerShell sibling, and exactly one runs per machine.
Expand Down
76 changes: 70 additions & 6 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
Marketplace source repo (default: qualifire-dev/rogue-plugins).
.PARAMETER NonInteractive
Fail / skip prompts rather than ask for missing values.
.PARAMETER Claude
Install only for Claude Code (combine with -Codex/-Cursor to pick a set).
.PARAMETER Codex
Install only for OpenAI Codex.
.PARAMETER Cursor
Install only for Cursor. With no agent switch, every detected agent is installed.
#>
[CmdletBinding()]
param(
Expand All @@ -39,7 +45,10 @@ param(
[string]$Name,
[string]$BaseUrl,
[string]$PluginRepo,
[switch]$NonInteractive
[switch]$NonInteractive,
[switch]$Claude,
[switch]$Codex,
[switch]$Cursor
)

$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -70,11 +79,28 @@ try {
Write-Host ""
Write-Host "Rogue Security (Windows)" -ForegroundColor Cyan

# Detect every supported agent on PATH; at least one is required.
$hasClaude = [bool](Get-Command claude -ErrorAction SilentlyContinue)
$hasCodex = [bool](Get-Command codex -ErrorAction SilentlyContinue)
if (-not ($hasClaude -or $hasCodex)) {
Die "No supported coding agent found on PATH (looked for: claude, codex). Install Claude Code (https://claude.com/code) or OpenAI Codex first."
# Agent selection. -Claude/-Codex/-Cursor pick an explicit set; with none, auto-detect
# every supported agent. claude/codex ship a CLI on PATH; Cursor's `cursor` command is
# opt-in, so detection also accepts %USERPROFILE%\.cursor. An explicitly selected CLI
# agent still needs its binary; Cursor is a plain file copy, so it installs regardless.
$explicit = $Claude -or $Codex -or $Cursor
if ($explicit) {
$hasClaude = [bool]$Claude
$hasCodex = [bool]$Codex
$hasCursor = [bool]$Cursor
if ($hasClaude -and -not (Get-Command claude -ErrorAction SilentlyContinue)) {
Die "-Claude requested but the 'claude' CLI is not on PATH. Install Claude Code (https://claude.com/code) first."
}
if ($hasCodex -and -not (Get-Command codex -ErrorAction SilentlyContinue)) {
Die "-Codex requested but the 'codex' CLI is not on PATH. Install OpenAI Codex first."
}
} else {
$hasClaude = [bool](Get-Command claude -ErrorAction SilentlyContinue)
$hasCodex = [bool](Get-Command codex -ErrorAction SilentlyContinue)
$hasCursor = [bool](Get-Command cursor -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:USERPROFILE '.cursor'))
if (-not ($hasClaude -or $hasCodex -or $hasCursor)) {
Die "No supported coding agent found (looked for: claude, codex, cursor). Install Claude Code (https://claude.com/code), OpenAI Codex, or Cursor (https://cursor.com) first."
}
}
# Claude shells out to git to clone the marketplace; git is required only for it.
if ($hasClaude -and -not (Get-Command git -ErrorAction SilentlyContinue)) {
Expand Down Expand Up @@ -216,6 +242,44 @@ if ($hasCodex) {
Warn2 'Codex skips untrusted hooks - open /hooks in Codex and trust the Rogue entries once.'
}

# Cursor has no plugin CLI: install is a file copy into
# %USERPROFILE%\.cursor\plugins\local\rogue. Download the release tarball, extract
# with `tar` (bundled in Windows 10+), and copy plugins\cursor into place. The Team
# Marketplace is the separate, admin-driven managed path; this does not touch it.
if ($hasCursor) {
Write-Host ""
Write-Host "Rogue Security - Cursor" -ForegroundColor Cyan
# Cursor ships dual dispatchers (sh + PowerShell) like Claude/Codex; the runtime
# is the same shell stack, so no extra prerequisite check beyond tar (below).
$asset = 'rogue-plugin-cursor.tar.gz'
if ($env:ROGUE_PLUGIN_VERSION) {
$url = "https://github.com/$PluginRepo/releases/download/$($env:ROGUE_PLUGIN_VERSION)/$asset"
} else {
$url = "https://github.com/$PluginRepo/releases/latest/download/$asset"
}
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("rogue-cursor-" + [System.IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
try {
Log "Downloading plugin $asset"
$tarball = Join-Path $tmp 'p.tar.gz'
Invoke-WebRequest -Uri $url -OutFile $tarball -UseBasicParsing -TimeoutSec 60 -ErrorAction Stop
& tar -xzf $tarball -C $tmp
if ($LASTEXITCODE -ne 0) { Die "Could not extract the Cursor plugin tarball (is 'tar' available?)." }
$src = Get-ChildItem -Path $tmp -Recurse -Directory -Filter 'cursor' |
Where-Object { Test-Path (Join-Path $_.FullName '.cursor-plugin\plugin.json') } |
Select-Object -First 1
if (-not $src) { Die "Cursor plugin manifest missing in download." }
$dest = Join-Path $env:USERPROFILE '.cursor\plugins\local\rogue'
if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
New-Item -ItemType Directory -Path $dest -Force | Out-Null
Copy-Item -Recurse -Force (Join-Path $src.FullName '*') $dest
Ok "Plugin installed -> $dest"
Warn2 'Fully quit and reopen Cursor, then run /rogue:status to verify.'
} finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}

Write-Host @"

v Rogue Security installed.
Expand Down
Loading
Loading