You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
vibecop hook now emits harness-native JSON on stdout instead of relying on
exit codes. None of Claude / Codex / Gemini / Copilot honor the legacy
exit-code contract — approve was a silent no-op and deny did not deny. This
implements the design at docs/superpowers/specs/2026-05-08-per-harness-hook-responses-design.md
and verifies the open detail (Codex/Copilot settings shapes) against current
official docs.
- internal/hooks/responder.go: pure-function WriteVerdict shapes the per-
(harness, event, verdict) JSON payload. Exits 0 always — fail-open is the
contract. Unknown combos suppress the [DENY]/[ESCALATE] stderr line.
- internal/hooks/hooks.go: NormalizedRequest gains Event; Codex/Copilot
payload structs + parsers; DetectAndParse uses hook_event_name with
per-harness defaults and a Codex-vs-Claude tiebreaker.
- internal/hooks/install.go: installCodexHooks registers under PreToolUse
AND PermissionRequest (PreToolUse cannot allow on Codex);
installCopilotHooks writes the flat preToolUse hook with the bash key.
- daemon.Request and daemon.Event gain Harness + HookEvent;
vibecop.harness + vibecop.hook_event span attrs;
vibecop.harness label on verdicts_total and evaluator_latency_ms.
- cmd/hook.go: replaces the verdict switch with
os.Exit(WriteVerdict(...)). cmd/install.go: --harness enum gains
codex|copilot; --all installs all four.
- docs/spec.md and AGENTS.md (invariant #3): replaced exit-code contract
with the per-harness JSON contract.
Tests: 155 pass (62 new across responder, parser, install, daemon round-
trip, telemetry attrs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: AGENTS.md
+8-7Lines changed: 8 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,7 +45,7 @@ These are load-bearing. Spec sections in parentheses.
45
45
46
46
1.**Fail-open everywhere** ("Failure Handling"). If anything in vibecop's own code fails — bad config, daemon down, parse error, LLM 500 — the hook MUST exit 0 so the user's coding agent is never blocked. The one exception: a *successful*`deny` or `escalate` verdict from the LLM exits 1.
47
47
2.**Three consecutive evaluator failures → suspended pass-through** for the rest of the daemon's life (`cmd/start.go`, `maxConsecutiveFailures`). Resume requires `vibecop test` (or restart). Do not change this without updating spec + test (`cmd/handler_test.go`).
48
-
3.**Exit-code contract** ("Exit code contract" table). `approve→0`, `deny→1`, `escalate→1`, timeout→1, daemon unreachable→0. Stderr text is part of the contract — Claude Code and Gemini CLI display it to the user.
48
+
3.**Per-harness JSON response contract** ("Verdict → harness response contract" / "Per-harness JSON shapes" tables). `vibecop hook` always exits 0 — the harness keys off the JSON written to stdout. `approve` emits harness-native "allow" JSON; `deny` emits harness-native "deny" JSON plus `VibeCop [DENY]: <reason>` on stderr; `escalate` emits no JSON and lets the harness's normal flow run. Fail-open paths (parse error, daemon unreachable, unknown harness/event/verdict, marshal failure) emit no JSON and exit 0. The `[DENY]` / `[ESCALATE]` stderr line is gated on the JSON-shaping success path — never written when we couldn't actually emit the deny.
49
49
4.**Project identity = SHA256 of absolute path** (`config.ProjectHash`). Do not hash the basename, do not normalize symlinks. Per-project storage at `~/.vibecop/projects/<hash>/`.
50
50
5.**Activity log is ephemeral, audit log is permanent.**`activity.jsonl` is a rolling window of last `activity_window` verdicts (default 10) used as LLM context. `audit/YYYY-MM-DD.jsonl` is the permanent record, only written when `audit_enabled = true`. Never read audit logs back into prompts.
51
51
6.**`think: false` for Ollama CoT models.** Local endpoints with reasoning models (`qwen3`, `deepseek-r1`) need this in the request body to avoid 30s+ latencies. The injection lives in `internal/evaluator/`.
@@ -66,12 +66,13 @@ These are load-bearing. Spec sections in parentheses.
66
66
67
67
## Adding a new harness (e.g. Deepseek)
68
68
69
-
1. Add a payload struct + parser in `internal/hooks/hooks.go` (mirror `ClaudeCodePayload` / `GeminiCLIPayload`).
70
-
2. Extend `DetectAndParse` and `parseWithFormat`.
71
-
3. Add subprocess invocation in `internal/evaluator/init.go` for the Guardian-prompt generation step.
72
-
4. Add idempotent settings-file patching in `internal/hooks/install.go`.
73
-
5. Update spec.md, README.md, this file, and `cmd/install.go`'s `--harness` enum.
1. Add a payload struct + parser in `internal/hooks/hooks.go` (mirror `ClaudeCodePayload` / `GeminiCLIPayload` / `CodexPayload` / `CopilotPayload`).
70
+
2. Extend `DetectAndParse`, `parseWithFormat`, and `defaultEventFor`.
71
+
3. Add the harness's row to `WriteVerdict` in `internal/hooks/responder.go` for each `(event, verdict)` combination, plus table-test coverage in `responder_test.go`.
72
+
4. Add subprocess invocation in `internal/evaluator/init.go` for the Guardian-prompt generation step.
73
+
5. Add idempotent settings-file patching in `internal/hooks/install.go`.
74
+
6. Update spec.md, README.md, this file, and `cmd/install.go`'s `--harness` enum.
installCmd.Flags().StringVar(&installHarness, "harness", "", "Harness to install into (claude|gemini) — use 'claude' for any claude-compatible wrapper")
82
+
installCmd.Flags().StringVar(&installHarness, "harness", "", "Harness to install into (claude|gemini|codex|copilot) — use 'claude' for any claude-compatible wrapper")
77
83
installCmd.Flags().BoolVar(&installAll, "all", false, "Install into all supported harnesses")
78
84
installCmd.Flags().StringVar(&installVibecopPath, "vibecop-path", "", "Path to a specific vibecop binary the hook should call (default: 'vibecop' via $PATH). Resolved to absolute.")
Copy file name to clipboardExpand all lines: docs/spec.md
+59-11Lines changed: 59 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -158,7 +158,7 @@ After subscribing, the daemon streams newline-terminated event objects to the TU
158
158
159
159
### Hook scripts
160
160
161
-
`vibecop install` writes thin wrapper scripts that delegate entirely to `vibecop hook`:
161
+
`vibecop install` writes thin wrapper config that delegates entirely to `vibecop hook`:
162
162
163
163
**Claude Code** (`~/.claude/settings.json` — adds a `PreToolUse` hook):
164
164
```json
@@ -174,6 +174,20 @@ After subscribing, the daemon streams newline-terminated event objects to the TU
174
174
}
175
175
```
176
176
177
+
**Codex CLI** (`~/.codex/hooks.json` — registers under both `PreToolUse` and `PermissionRequest`; PreToolUse cannot allow on Codex, so PermissionRequest is the only event that can silently approve):
@@ -183,19 +197,53 @@ After subscribing, the daemon streams newline-terminated event objects to the TU
183
197
}
184
198
```
185
199
186
-
`vibecop hook` reads the harness's JSON payload from stdin, normalizes it to the IPC request format, sends it to the daemon socket, and exits according to the exit code contract. The harness is auto-detected from the input payload shape; it can also be overridden with `--harness`.
200
+
**Copilot CLI** (`~/.copilot/settings.json` — flat array of hook definitions, command lives under `bash`):
201
+
```json
202
+
{
203
+
"version": 1,
204
+
"hooks": {
205
+
"preToolUse": [
206
+
{ "type": "command", "bash": "vibecop hook" }
207
+
]
208
+
}
209
+
}
210
+
```
187
211
188
-
### Exit code contract
212
+
`vibecop hook` reads the harness's JSON payload from stdin, normalizes it to the IPC request format, sends it to the daemon socket, and emits the harness-native JSON response on stdout. The harness is auto-detected from the input payload shape; it can also be overridden with `--harness {claude|gemini|codex|copilot}`.
189
213
190
-
| Verdict | Exit code | Stderr |
191
-
|---|---|---|
192
-
|`approve`|`0`| silent |
193
-
|`deny`|`1`|`VibeCop [DENY]: <reason>`|
194
-
|`escalate`|`1`|`VibeCop [ESCALATE]: <reason>`|
195
-
| timeout |`1`|`VibeCop: timed out after <N>ms — escalating`|
196
-
| daemon unreachable |`0`| silent (fail-open) |
214
+
### Verdict → harness response contract
215
+
216
+
`vibecop hook` always exits 0 — the JSON it writes to stdout is what the harness keys off. The exit code is reserved for fail-open: a parse error, a marshal failure, an unrecognized harness/event, or an unknown verdict all produce no stdout and exit 0 with a single-line stderr diagnostic.
|`approve`| Emit harness-native "allow" JSON. Skips the harness's user prompt. |
221
+
|`deny`| Emit harness-native "deny" JSON. Tool is blocked. Stderr `VibeCop [DENY]: <reason>` is preserved for operator visibility. |
222
+
|`escalate`| Emit no JSON. The harness's normal permission flow runs (typically prompts the user). Stderr `VibeCop [ESCALATE]: <reason>` is preserved when reason is non-empty. |
223
+
224
+
`escalate` deliberately does not map to a harness's "ask" decision: vibecop's preference is "no objection, defer to whatever the harness would have done" rather than forcing a prompt the harness might otherwise have skipped under a user-defined rule.
225
+
226
+
#### Per-harness JSON shapes
227
+
228
+
Reason fields are omitted when the daemon's reason is empty.
|`codex`, `PreToolUse`| (no stdout — Codex `PreToolUse` cannot allow; `PermissionRequest` is the approval channel) |`{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"…"}}`|
-**JSON marshal failure in `WriteVerdict`** — fail-open: no stdout, exit 0.
243
+
-**Unknown `(harness, event)` or unknown verdict** — fail-open: no stdout, single-line stderr diagnostic, exit 0. The `[DENY]` / `[ESCALATE]` stderr line is suppressed in this case so the operator's terminal isn't lying about a block that didn't happen.
244
+
-**Three-consecutive evaluator failures** — daemon-side suspension (unchanged from "Failure Handling" below); the hook still emits per-harness responses, just always `approve`.
197
245
198
-
Non-zero exit causes the coding harness to surface its own native permission prompt. The stderr text is displayed to the user as context. The human always has final say — `deny` is a strong recommendation, not a unilateral block.
246
+
The human always has final say. `deny` is a strong recommendation that the harness can override (e.g. Claude Code's "approve anyway" affordance after seeing the deny reason).
0 commit comments