Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 93fc08b

Browse files
authored
chore(agent): pin rtk signing-flow invariants with tests (#3255)
1 parent 6444db5 commit 93fc08b

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

packages/agent/src/adapters/claude/session/options.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as fs from "node:fs";
12
import * as os from "node:os";
23
import * as path from "node:path";
34
import type { HookInput, Options } from "@anthropic-ai/claude-agent-sdk";
@@ -141,6 +142,88 @@ describe("buildSessionOptions", () => {
141142
expect(healSpy).not.toHaveBeenCalled();
142143
});
143144

145+
describe("rtk and signed-commit guard ordering", () => {
146+
const originalRtk = process.env.POSTHOG_RTK;
147+
let dir: string;
148+
let binary: string;
149+
150+
beforeEach(() => {
151+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "rtk-order-"));
152+
binary = path.join(dir, "rtk");
153+
fs.writeFileSync(binary, "#!/bin/sh\n");
154+
process.env.POSTHOG_RTK = binary;
155+
});
156+
157+
afterEach(() => {
158+
if (originalRtk === undefined) {
159+
delete process.env.POSTHOG_RTK;
160+
} else {
161+
process.env.POSTHOG_RTK = originalRtk;
162+
}
163+
fs.rmSync(dir, { recursive: true, force: true });
164+
});
165+
166+
const bashInput = (command: string): HookInput =>
167+
({
168+
...(GIT_COMMIT_HOOK_INPUT as object),
169+
tool_input: { command },
170+
}) as HookInput;
171+
172+
type PreToolUseOutput = {
173+
hookSpecificOutput?: {
174+
permissionDecision?: string;
175+
updatedInput?: { command?: string };
176+
};
177+
};
178+
179+
it("registers the signed-commit guard before the rtk rewrite so the guard evaluates raw commands (cloud)", async () => {
180+
const options = buildSessionOptions({
181+
...makeParams(),
182+
cloudMode: true,
183+
});
184+
const hooks = (options.hooks?.PreToolUse ?? []).flatMap(
185+
(entry) => entry.hooks ?? [],
186+
);
187+
const opts = { signal: new AbortController().signal };
188+
189+
// Identify each hook behaviorally: the guard denies `git commit`, the
190+
// rtk hook rewrites `git status`. Their registration order is the
191+
// defense-in-depth guarantee that the guard always sees the raw command.
192+
let guardIndex = -1;
193+
let rtkIndex = -1;
194+
for (const [index, hook] of hooks.entries()) {
195+
const denyResult = (await hook(
196+
bashInput("git commit -m x"),
197+
undefined,
198+
opts,
199+
)) as PreToolUseOutput;
200+
if (
201+
guardIndex === -1 &&
202+
denyResult.hookSpecificOutput?.permissionDecision === "deny"
203+
) {
204+
guardIndex = index;
205+
}
206+
207+
const rewriteResult = (await hook(
208+
bashInput("git status"),
209+
undefined,
210+
opts,
211+
)) as PreToolUseOutput;
212+
if (
213+
rtkIndex === -1 &&
214+
rewriteResult.hookSpecificOutput?.updatedInput?.command ===
215+
`${binary} git status`
216+
) {
217+
rtkIndex = index;
218+
}
219+
}
220+
221+
expect(guardIndex).toBeGreaterThanOrEqual(0);
222+
expect(rtkIndex).toBeGreaterThanOrEqual(0);
223+
expect(guardIndex).toBeLessThan(rtkIndex);
224+
});
225+
});
226+
144227
describe("CLAUDE_CODE_EXECUTABLE", () => {
145228
const originalClaudeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
146229

packages/agent/src/adapters/claude/session/rtk.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ describe("rewriteBashForRtk", () => {
3131
["git commit -m wip"],
3232
["git push origin main"],
3333
["git checkout -b feature"],
34+
// The cloud signed-commit flow instructs the model to run these raw:
35+
// staging before git_signed_commit, and the stale-checkout / rebase
36+
// recovery sequence. They must never enter the compressible allowlist.
37+
["git add -A"],
38+
["git stash --include-untracked"],
39+
["git stash pop"],
40+
["git fetch origin main"],
41+
["git reset --hard origin/main"],
42+
["git rebase --continue"],
43+
["git merge origin/master"],
44+
["git cherry-pick abc123"],
3445
// Commands RTK isn't wrapping in this cut.
3546
["npm test"],
3647
["cat file.ts"],

0 commit comments

Comments
 (0)