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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ open-plan-annotator agent-setup
open-plan-annotator help agent
```

### Skipping the Annotator

Set `OPEN_PLAN_ANNOTATOR_SKIP_FILE` to a file path. While that file exists, the Claude Code hook exits without a decision and Claude Code shows its native plan approval instead. The file is checked on every hook call, so you can toggle it mid session, for example while continuing a session through Remote Control where the local browser UI is unreachable:

```sh
export OPEN_PLAN_ANNOTATOR_SKIP_FILE="$HOME/.open-plan-annotator-skip"
touch "$OPEN_PLAN_ANNOTATOR_SKIP_FILE" # use native approval
rm "$OPEN_PLAN_ANNOTATOR_SKIP_FILE" # back to the annotator
```

## Updates

- **Claude Code**: navigate to the `/plugin` -> "Installed" -> `open-plan-annotator` entry and select "Update", then restart Claude Code.
Expand Down
6 changes: 6 additions & 0 deletions bin/open-plan-annotator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { codesignFixCommand, ensureValidCodeSignature, hasValidCodeSignature } f
import { detectPackageManager } from "../shared/packageManager.mjs";
import { resolveRuntimeBinary } from "../shared/runtimeResolver.mjs";
import { buildRuntimeEnv } from "../shared/runtimeEnv.mjs";
import { shouldSkipHook } from "../shared/skipHook.mjs";
import { buildUpdateMessage } from "../shared/updateMessage.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -51,6 +52,10 @@ if (cliMode === "unknown") {
process.exit(1);
}

if (cliMode === "hook" && shouldSkipHook()) {
process.exit(0);
}

// Buffer stdin immediately so it's not lost if we need to download first.
// Skip when stdin is a TTY (manual invocation) to avoid blocking forever.
let stdinBuffer;
Expand Down Expand Up @@ -102,6 +107,7 @@ function runRuntime(binaryPath, { allowResign }) {
env: childEnv,
});

child.stdin.on("error", () => {});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this fixes a test that is also faiing on main

child.stdin.write(stdinBuffer);
child.stdin.end();

Expand Down
15 changes: 13 additions & 2 deletions bin/open-plan-annotator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ interface WrapperResult {
stdioClosedAfterMs: number;
}

function runHook(root: string): Promise<WrapperResult> {
function runHook(root: string, env: NodeJS.ProcessEnv = {}): Promise<WrapperResult> {
const start = Date.now();
const child = spawn("node", [path.join(root, "bin", "open-plan-annotator.mjs")], {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, OPEN_PLAN_ANNOTATOR_SKIP_INSTALL: "1" },
env: { ...process.env, OPEN_PLAN_ANNOTATOR_SKIP_INSTALL: "1", ...env },
});
child.stdin.end(JSON.stringify({ hook_event_name: "PreToolUse", tool_name: "ExitPlanMode", tool_input: {} }));

Expand Down Expand Up @@ -87,4 +87,15 @@ describe.if(process.platform !== "win32")("open-plan-annotator wrapper (hook mod
expect(result.stdout).toBe('{"ok":true}\n');
expect(result.stdioClosedAfterMs).toBeLessThan(2000);
});

test("exits 0 without running the runtime when the skip file exists", async () => {
const root = createPluginTree("#!/bin/sh\necho '{\"ok\":true}'\n");
const skipFile = path.join(root, "skip");
fs.writeFileSync(skipFile, "");

const result = await runHook(root, { OPEN_PLAN_ANNOTATOR_SKIP_FILE: skipFile });

expect(result.code).toBe(0);
expect(result.stdout).toBe("");
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"shared/runtimeResolver.mjs",
"shared/macosCodesign.mjs",
"shared/runtimeEnv.mjs",
"shared/skipHook.mjs",
"shared/updateHints.mjs",
"shared/versionInfo.mjs",
"shared/planReview.mjs",
Expand Down
10 changes: 10 additions & 0 deletions shared/skipHook.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import fs from "node:fs";

/**
* @param {NodeJS.ProcessEnv} [env]
* @returns {boolean}
*/
export function shouldSkipHook(env = process.env) {
const skipFile = env.OPEN_PLAN_ANNOTATOR_SKIP_FILE;
return Boolean(skipFile) && fs.existsSync(skipFile);
}
22 changes: 22 additions & 0 deletions shared/skipHook.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, test } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { shouldSkipHook } from "./skipHook.mjs";

describe("shouldSkipHook", () => {
test("does not skip when the env var is unset", () => {
expect(shouldSkipHook({})).toBe(false);
});

test("does not skip when the file is missing", () => {
const skipFile = path.join(os.tmpdir(), "opa-skip-missing");
expect(shouldSkipHook({ OPEN_PLAN_ANNOTATOR_SKIP_FILE: skipFile })).toBe(false);
});

test("skips when the file exists", () => {
const skipFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "opa-skip-")), "skip");
fs.writeFileSync(skipFile, "");
expect(shouldSkipHook({ OPEN_PLAN_ANNOTATOR_SKIP_FILE: skipFile })).toBe(true);
});
});
Loading