diff --git a/packages/core/src/git-interaction/errorPrompts.ts b/packages/core/src/git-interaction/errorPrompts.ts
index 05d0683156..fb4994e2d9 100644
--- a/packages/core/src/git-interaction/errorPrompts.ts
+++ b/packages/core/src/git-interaction/errorPrompts.ts
@@ -5,6 +5,27 @@ export interface FixWithAgentPrompt {
context: string;
}
+export function buildCommitHookErrorPrompt(
+ command: string,
+): FixWithAgentPrompt {
+ return {
+ label: `Fix commit failure`,
+ context: `A \`git commit\` command failed, most likely because a pre-commit hook (e.g. lint, format, type-check, or secret scan) rejected the commit or exited with a non-zero status. The command that failed was:
+
+${command}
+
+The full hook output is below.
+
+Your task is to diagnose the root cause from the output and fix the underlying issue (e.g. lint/format violations, failing checks, missing dependencies) so the commit can succeed.
+
+IMPORTANT:
+- Start by reading the output carefully to identify which hook failed and why.
+- Fix the underlying issue rather than bypassing the hook. Do NOT use \`--no-verify\` unless the user explicitly asks for it.
+- Confirm any destructive actions with the user first.
+- Once the issue is resolved, you may retry the commit.`,
+ };
+}
+
export function buildCreatePrFlowErrorPrompt(
failedStep: CreatePrStep | null,
): FixWithAgentPrompt {
diff --git a/packages/ui/src/features/sessions/components/session-update/CommitFailureActions.tsx b/packages/ui/src/features/sessions/components/session-update/CommitFailureActions.tsx
new file mode 100644
index 0000000000..15e534309c
--- /dev/null
+++ b/packages/ui/src/features/sessions/components/session-update/CommitFailureActions.tsx
@@ -0,0 +1,57 @@
+import { Check, Copy, Sparkle } from "@phosphor-icons/react";
+import { buildCommitHookErrorPrompt } from "@posthog/core/git-interaction/errorPrompts";
+import { useFixWithAgent } from "@posthog/ui/features/git-interaction/useFixWithAgent";
+import { Button, Flex } from "@radix-ui/themes";
+import { useState } from "react";
+
+const ICON_SIZE = 12;
+
+/**
+ * Action footer shown under a failed `git commit` Bash tool call (e.g. a
+ * pre-commit hook rejected the commit). Lets the user hand the failure output
+ * to the agent to resolve, or copy it to paste into a new message.
+ */
+export function CommitFailureActions({
+ command,
+ output,
+}: {
+ command: string;
+ output: string;
+}) {
+ const { canFixWithAgent, fixWithAgent } = useFixWithAgent(() =>
+ buildCommitHookErrorPrompt(command),
+ );
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(output);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // Clipboard writes can reject when the document isn't focused; ignore.
+ }
+ };
+
+ return (
+
+ {canFixWithAgent && (
+
+ )}
+
+
+ );
+}
diff --git a/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.test.tsx b/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.test.tsx
new file mode 100644
index 0000000000..9d1d910ba6
--- /dev/null
+++ b/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.test.tsx
@@ -0,0 +1,93 @@
+import type { ToolCall } from "@posthog/ui/features/sessions/types";
+import { Theme } from "@radix-ui/themes";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { ExecuteToolView } from "./ExecuteToolView";
+
+vi.mock("./CommitFailureActions", () => ({
+ CommitFailureActions: ({
+ command,
+ output,
+ }: {
+ command: string;
+ output: string;
+ }) => (
+
+ ),
+}));
+
+function makeToolCall(overrides: Partial = {}): ToolCall {
+ return {
+ toolCallId: "tc-1",
+ title: "bash",
+ kind: "execute",
+ status: "completed",
+ rawInput: {
+ command: 'git commit -m "test"',
+ description: "run commit",
+ },
+ content: [],
+ ...overrides,
+ };
+}
+
+function textContent(text: string): NonNullable {
+ return [{ type: "content", content: { type: "text", text } }];
+}
+
+function renderView(toolCall: ToolCall) {
+ return render(
+
+
+ ,
+ );
+}
+
+describe("ExecuteToolView", () => {
+ it("shows commit failure actions for a git commit failure with hook output", () => {
+ renderView(
+ makeToolCall({
+ status: "failed",
+ rawInput: { command: 'git commit -m "test"' },
+ content: textContent(`Exit code 1\n╭───╮\n│ hook: pre-commit │\n╰───╯`),
+ }),
+ );
+
+ const actions = screen.getByTestId("commit-failure-actions");
+ expect(actions).toHaveAttribute("data-command", 'git commit -m "test"');
+ expect(actions).toHaveAttribute(
+ "data-output",
+ expect.stringContaining("hook: pre-commit"),
+ );
+ });
+
+ it("still shows commit failure actions when the tool status is completed but the output has a non-zero exit signal", () => {
+ renderView(
+ makeToolCall({
+ status: "completed",
+ rawInput: { command: "pre-commit run --all-files" },
+ content: textContent("exit status 1\npre-commit failed"),
+ }),
+ );
+
+ expect(screen.getByTestId("commit-failure-actions")).toBeInTheDocument();
+ });
+
+ it("does not show commit failure actions for unrelated failed bash output", () => {
+ renderView(
+ makeToolCall({
+ status: "failed",
+ rawInput: { command: "git push origin main" },
+ content: textContent("exit status 1\nnetwork error"),
+ }),
+ );
+
+ expect(
+ screen.queryByTestId("commit-failure-actions"),
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.tsx b/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.tsx
index 2217f02bdb..d87b9e8b97 100644
--- a/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.tsx
+++ b/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.tsx
@@ -1,5 +1,6 @@
import { Terminal } from "@phosphor-icons/react";
import { compactHomePath } from "@posthog/shared";
+import { CommitFailureActions } from "./CommitFailureActions";
import { ToolRow } from "./ToolRow";
import {
ContentPre,
@@ -14,6 +15,28 @@ import {
const ANSI_REGEX = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;]*m`, "g");
const MAX_COMMAND_LENGTH = 120;
+const GIT_COMMIT_COMMAND_REGEX =
+ /(?:^|[\s;&|(])git(?:\s+-[cC]\s+\S+|\s+-\S+)*\s+commit(?:\s|$)/;
+
+const PRE_COMMIT_COMMAND_REGEX =
+ /\blefthook\b[\s\S]*?\bpre-commit\b|\bpre-commit\b(?:[\s\S]*?\brun\b|$)|\bhusky\b(?:\s+run\b|[\s\S]*?\bpre-commit\b)|\bovercommit\b[\s\S]*?(?:--run\b|\bpre-commit\b)|\blint-staged\b|[./]*\.(?:git\/hooks|husky)\/pre-commit\b/i;
+
+const PRE_COMMIT_OUTPUT_REGEX =
+ /hook:\s*pre-commit\b|\blefthook\b.*\b(?:hook|run)\b|\bRunning\s+(?:pre-commit\b.*\bhooks?\b|\bhooks?\b.*\bpre-commit\b)|\[pre-commit\]|\bhusky\b.*\bpre-commit\b/i;
+
+const EXIT_STATUS_SIGNAL_REGEX =
+ /\b(?:exit\s+(?:status|code)|exited\s+with\s+(?:code|status))[:\s]+(\d+)\b/gi;
+
+function hasNonZeroExitStatusSignal(text: string): boolean {
+ for (const match of text.matchAll(EXIT_STATUS_SIGNAL_REGEX)) {
+ const exitCode = Number(match[1]);
+ if (Number.isFinite(exitCode) && exitCode > 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
interface ExecuteRawInput {
command?: string;
description?: string;
@@ -42,6 +65,15 @@ export function ExecuteToolView({
"",
);
const hasOutput = output.trim().length > 0;
+ const isGitCommitCommand = GIT_COMMIT_COMMAND_REGEX.test(command);
+ const isPreCommitRunCommand = PRE_COMMIT_COMMAND_REGEX.test(command);
+ const hasPreCommitSignal = PRE_COMMIT_OUTPUT_REGEX.test(output);
+ const hasNonZeroExitSignal = hasNonZeroExitStatusSignal(output);
+ const hasFailureSignal = isFailed || hasNonZeroExitSignal;
+ const showCommitFailureActions =
+ hasFailureSignal &&
+ hasOutput &&
+ ((isGitCommitCommand && hasPreCommitSignal) || isPreCommitRunCommand);
return (
{output} : undefined}
+ content={
+ hasOutput ? (
+ <>
+ {output}
+ {showCommitFailureActions && (
+
+ )}
+ >
+ ) : undefined
+ }
>
{description && {description}}
{command && (