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
12 changes: 12 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,35 @@ subscribe to it for parity with `agentStop`).

Ref: <https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-hooks-reference>

**1.0.71 contract re-verification** (2026-07-16, live recorder-hook captures —
Copilot's schema drifted since the 1.0.41 verification above):

- **Config**: `hooks` must be an **object** keyed by event. Older installed
files were rejected wholesale (`[ERROR] Invalid hook configuration …: hooks
must be an object`) and the session ran **unhooked** — silently, no user-visible
warning. Reinstalling with the current `writeHookEntries` schema fixes it.
- **Payloads**: the snake_case events (PreToolUse/PostToolUse/Stop/…) are
Claude-shaped and deliver `tool_name` **already canonical** (`Bash`, `Read`,
`Write`, `Edit`, `Grep`) — but the file tools use Copilot's own input keys:
Read `{path}`, Write `{path, file_text}`, Edit `{path, old_str, new_str}`.
`COPILOT_TOOL_INPUT_MAP` (keyed by canonical name) maps them to
`file_path`/`content`/`old_string`/`new_string`; without it a live `.env`
read passed block-env-files. Bash `{command}` and Grep `{pattern}` are canonical.
- **`permissionRequest` is the camelCase exception**: `{hookName, sessionId,
timestamp:number, cwd, toolName:"bash" (lowercase), toolInput,
permissionSuggestions}`. `handler.ts` has a `cli === "copilot"` branch
normalizing `toolName`/`toolInput`/`sessionId`; the lowercase names go
through `COPILOT_TOOL_MAP` as before.
- **Deny still works**: the Claude `{hookSpecificOutput:{permissionDecision:
"deny"}}` shape is honored on PreToolUse — verified live (`success:false,
code:"denied"` in session events; the sudo never ran). Stop gates
(`require-*-before-stop`) also verified firing + forcing retries on 1.0.71.
- **Caveat**: in headless `copilot -p` runs from a fresh directory, the
project-scope `.github/hooks/*.json` file was NOT loaded (git repo or not);
the user-scope `~/.copilot/hooks/*.json` always loaded. Likely a
trusted-folder gate — treat user scope as the reliable enforcement point for
headless/CI usage until verified otherwise.

### Cursor hooks (`.cursor/hooks.json`)

This repo also ships a `.cursor/hooks.json` for Cursor Agent CLI sessions,
Expand Down
167 changes: 93 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,80 +25,99 @@ before they become incidents. Zero latency. Runs locally.

## Supported agent CLIs

<p align="center">
<a href="https://claude.com/claude-code" title="Claude Code">
<img src="assets/logos/claude.svg" alt="Claude Code" width="64" height="64" />
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://learn.chatgpt.com" title="OpenAI Codex">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/openai-dark.svg" />
<img src="assets/logos/openai-light.svg" alt="OpenAI Codex" width="64" height="64" />
</picture>
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://github.com/features/copilot/cli" title="GitHub Copilot CLI">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/copilot-dark.svg" />
<img src="assets/logos/copilot-light.svg" alt="GitHub Copilot" width="64" height="64" />
</picture>
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://cursor.com" title="Cursor Agent CLI">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/cursor-dark.svg" />
<img src="assets/logos/cursor-light.svg" alt="Cursor Agent" width="64" height="64" />
</picture>
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://opencode.ai/" title="OpenCode">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/opencode-dark.svg" />
<img src="assets/logos/opencode-light.svg" alt="OpenCode" width="64" height="64" />
</picture>
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://pi.dev/" title="Pi (pi-coding-agent)">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/pi-dark.svg" />
<img src="assets/logos/pi-light.svg" alt="Pi" width="64" height="64" />
</picture>
</a>
</p>
<p align="center">
<a href="https://hermes-agent.nousresearch.com/" title="Hermes (hermes-agent)">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/hermes-dark.svg" />
<img src="assets/logos/hermes-light.svg" alt="Hermes" width="64" height="64" />
</picture>
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://openclaw.ai/" title="OpenClaw (openclaw gateway)">
<img src="assets/logos/openclaw.svg" alt="OpenClaw" width="64" height="64" />
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://factory.ai/" title="Factory Droid (droid)">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/factory-dark.png" />
<img src="assets/logos/factory-light.png" alt="Factory Droid" width="64" height="64" />
</picture>
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://devin.ai" title="Devin CLI (Cognition)">
<img src="assets/logos/devin.svg" alt="Devin CLI" width="64" height="64" />
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://antigravity.google" title="Antigravity CLI (agy)">
<img src="assets/logos/antigravity.svg" alt="Antigravity CLI" width="64" height="64" />
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://goose-docs.ai/" title="Goose (codename goose)">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/goose-dark.svg" />
<img src="assets/logos/goose-light.svg" alt="Goose" width="64" height="64" />
</picture>
</a>
</p>
<!-- A 6-column table instead of inline <img> runs: table columns never re-wrap,
so the grid stays 2×6 at any window width (scrolling on very narrow screens
instead of collapsing into ragged orphan rows). -->
<table align="center">
<tr>
<td align="center" width="96">
<a href="https://claude.com/claude-code" title="Claude Code">
<img src="assets/logos/claude.svg" alt="Claude Code" width="56" height="56" />
</a>
</td>
<td align="center" width="96">
<a href="https://learn.chatgpt.com" title="OpenAI Codex">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/openai-dark.svg" />
<img src="assets/logos/openai-light.svg" alt="OpenAI Codex" width="56" height="56" />
</picture>
</a>
</td>
<td align="center" width="96">
<a href="https://github.com/features/copilot/cli" title="GitHub Copilot CLI">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/copilot-dark.svg" />
<img src="assets/logos/copilot-light.svg" alt="GitHub Copilot" width="56" height="56" />
</picture>
</a>
</td>
<td align="center" width="96">
<a href="https://cursor.com" title="Cursor Agent CLI">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/cursor-dark.svg" />
<img src="assets/logos/cursor-light.svg" alt="Cursor Agent" width="56" height="56" />
</picture>
</a>
</td>
<td align="center" width="96">
<a href="https://opencode.ai/" title="OpenCode">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/opencode-dark.svg" />
<img src="assets/logos/opencode-light.svg" alt="OpenCode" width="56" height="56" />
</picture>
</a>
</td>
<td align="center" width="96">
<a href="https://pi.dev/" title="Pi (pi-coding-agent)">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/pi-dark.svg" />
<img src="assets/logos/pi-light.svg" alt="Pi" width="56" height="56" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" width="96">
<a href="https://hermes-agent.nousresearch.com/" title="Hermes (hermes-agent)">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/hermes-dark.svg" />
<img src="assets/logos/hermes-light.svg" alt="Hermes" width="56" height="56" />
</picture>
</a>
</td>
<td align="center" width="96">
<a href="https://openclaw.ai/" title="OpenClaw (openclaw gateway)">
<img src="assets/logos/openclaw.svg" alt="OpenClaw" width="56" height="56" />
</a>
</td>
<td align="center" width="96">
<a href="https://factory.ai/" title="Factory Droid (droid)">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/factory-dark.png" />
<img src="assets/logos/factory-light.png" alt="Factory Droid" width="56" height="56" />
</picture>
</a>
</td>
<td align="center" width="96">
<a href="https://devin.ai" title="Devin CLI (Cognition)">
<img src="assets/logos/devin.svg" alt="Devin CLI" width="56" height="56" />
</a>
</td>
<td align="center" width="96">
<a href="https://antigravity.google" title="Antigravity CLI (agy)">
<img src="assets/logos/antigravity.svg" alt="Antigravity CLI" width="56" height="56" />
</a>
</td>
<td align="center" width="96">
<a href="https://goose-docs.ai/" title="Goose (codename goose)">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/goose-dark.svg" />
<img src="assets/logos/goose-light.svg" alt="Goose" width="56" height="56" />
</picture>
</a>
</td>
</tr>
</table>

> Install hooks for one or any combination: `failproofai policies --install --cli opencode pi` (or `--cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose`). Omit `--cli` to auto-detect installed CLIs and prompt.
>
Expand Down
4 changes: 4 additions & 0 deletions __tests__/api/audit-run-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ describe("POST /api/audit/run (fire-and-forget)", () => {
expect(s.error).toBe("scan blew up");
expect(writeCacheMock).not.toHaveBeenCalled();
expect(trackedNames()).toContain("audit_run_failed");
expect(trackEventMock).toHaveBeenCalledWith(
"audit_run_failed",
expect.objectContaining({ error_type: "Error", error_message: "scan blew up" }),
);
});

it("writes the cache, tracks audit_run_completed with metrics, and clears the lock on success", async () => {
Expand Down
1 change: 1 addition & 0 deletions __tests__/audit/audit-cli-telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ describe("failproofai audit telemetry", () => {
expect(h.trackHookEvent).toHaveBeenCalledWith("test-instance", "cli_audit_failed", {
source: "cli",
error_type: "TypeError",
error_message: "disk exploded",
});
expect(exitInfo?.code).toBe(1);
// The fix: failed must have RESOLVED before the exit fired.
Expand Down
25 changes: 23 additions & 2 deletions __tests__/e2e/helpers/cli-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,30 @@
import { spawnSync } from "node:child_process";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { expect } from "vitest";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { afterAll, expect } from "vitest";

const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");

// Isolated HOME so config-mutating commands (`policies --install/--uninstall`,
// `policy add/remove`, `configure`) resolve `~/.failproofai` and `~/.claude`
// under a throwaway dir instead of the developer's real home. Without this, any
// runCli call that reaches installHooks/removeHooks clobbers the real user
// config (Bun honors the spawn-env HOME, so overriding it here fully isolates).
// Created once per test module; the OS reclaims it from tmp.
const ISOLATED_HOME = mkdtempSync(resolve(tmpdir(), "failproofai-cli-e2e-"));

// Remove the throwaway HOME after the importing suite finishes so repeated e2e
// runs don't accumulate config artifacts in tmp (mkdtempSync leaves it behind).
afterAll(() => {
try {
rmSync(ISOLATED_HOME, { recursive: true, force: true });
} catch {
// best-effort — the OS reclaims tmp regardless
}
});

function getBinaryPath(): string {
return resolve(REPO_ROOT, "bin/failproofai.mjs");
}
Expand All @@ -37,6 +56,8 @@ export function runCli(...args: string[]): CliRunResult {
const result = spawnSync("bun", [binaryPath, ...args], {
env: {
...process.env,
HOME: ISOLATED_HOME,
USERPROFILE: ISOLATED_HOME,
FAILPROOFAI_TELEMETRY_DISABLED: "1",
},
encoding: "utf8",
Expand Down
Loading